Claude Code for GraphQL: Schema Design, Resolvers, and Client Integration — Claude Skills 360 Blog
Blog / Development / Claude Code for GraphQL: Schema Design, Resolvers, and Client Integration
Development

Claude Code for GraphQL: Schema Design, Resolvers, and Client Integration

Published: May 11, 2026
Read time: 9 min read
By: Claude Skills 360

GraphQL development has two distinct layers of complexity: the schema design (which types, which queries, which relationships) and the implementation (resolvers, DataLoader, N+1 queries). Claude Code handles both because it reads your existing schema and generates resolvers that match your actual types — not generic resolver boilerplate.

This guide covers GraphQL development with Claude Code: schema design, resolver patterns, the N+1 problem, subscriptions, and TypeScript codegen integration.

Setting Up Claude Code for GraphQL Projects

Schema context in CLAUDE.md matters:

# GraphQL Project Context

## Stack
- Apollo Server 4, Node.js 20
- TypeScript + graphql-codegen for type generation
- Database: PostgreSQL via Prisma
- Auth: JWT in Authorization header
- Real-time: Apollo subscriptions via WebSocket

## Schema Conventions
- IDs: always String (not Int) — base64-encoded globally unique IDs
- Pagination: Relay cursor-based connections (not offset)
- Mutations: return the mutated type, not a success flag
- Errors: union types (AuthError | ValidationError | Success)
- Nullability: nullable fields mean "may not be loaded", non-null means "always exists"

## Never
- Return Boolean from mutations — return the affected object
- resolver-level data fetching without DataLoader (N+1)
- Expose internal database IDs directly

See the CLAUDE.md setup guide for full configuration.

Schema Design

Domain Modeling

Design a GraphQL schema for a project management tool.
Projects have tasks, tasks have assignees and comments.
Users can be members of multiple projects with different roles.
type Query {
  project(id: ID!): Project
  projects(filter: ProjectFilter, first: Int, after: String): ProjectConnection!
  me: User
}

type Mutation {
  createProject(input: CreateProjectInput!): CreateProjectResult!
  createTask(input: CreateTaskInput!): CreateTaskResult!
  updateTaskStatus(id: ID!, status: TaskStatus!): UpdateTaskResult!
  addProjectMember(projectId: ID!, userId: ID!, role: ProjectRole!): AddMemberResult!
}

type Subscription {
  taskUpdated(projectId: ID!): Task!
  memberAdded(projectId: ID!): ProjectMember!
}

type Project {
  id: ID!
  name: String!
  description: String
  status: ProjectStatus!
  members: [ProjectMember!]!
  tasks(filter: TaskFilter, first: Int, after: String): TaskConnection!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Task {
  id: ID!
  title: String!
  description: String
  status: TaskStatus!
  priority: Priority!
  assignee: User
  project: Project!
  comments(first: Int, after: String): CommentConnection!
  dueDate: DateTime
  createdAt: DateTime!
}

type ProjectMember {
  user: User!
  project: Project!
  role: ProjectRole!
  joinedAt: DateTime!
}

type User {
  id: ID!
  name: String!
  email: String!
  avatarUrl: String
  projects: [Project!]!
}

enum TaskStatus { BACKLOG TODO IN_PROGRESS IN_REVIEW DONE }
enum Priority { LOW MEDIUM HIGH URGENT }
enum ProjectRole { VIEWER CONTRIBUTOR ADMIN OWNER }
enum ProjectStatus { ACTIVE ARCHIVED }

Claude designs the schema with Relay-style connections for paginated lists, enum types for status/role fields, and returns full objects from mutations (not booleans). The ProjectMember join type captures the many-to-many relationship with additional data (role, joinedAt).

Error Handling via Union Types

Mutations can fail with auth errors or validation errors.
Model this properly in the schema — not with nullable returns.
union CreateProjectResult = Project | AuthError | ValidationError | RateLimitError

type AuthError {
  message: String!
  code: String!
}

type ValidationError {
  message: String!
  fieldErrors: [FieldError!]!
}

type FieldError {
  field: String!
  message: String!
}

type RateLimitError {
  message: String!
  retryAfter: Int!
}

Union result types make errors explicit in the schema. Clients must handle each case — no surprises from nullable returns. Apollo Client and URQL handle union types natively with __typename pattern matching.

Resolvers and DataLoader

The N+1 Problem

Without DataLoader, a query for 20 projects with their members triggers 21 queries (1 + 20). Claude generates DataLoader patterns by default:

Write the resolvers for Project.members and Task.assignee.
Use DataLoader to batch the database queries.
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';

// Created once per request, not globally
export function createLoaders(prisma: PrismaClient) {
  return {
    projectMembersLoader: new DataLoader(async (projectIds: readonly string[]) => {
      const members = await prisma.projectMember.findMany({
        where: { projectId: { in: projectIds as string[] } },
        include: { user: true },
      });
      
      // Group by projectId, maintaining order
      const memberMap = new Map<string, typeof members>();
      projectIds.forEach(id => memberMap.set(id, []));
      members.forEach(m => memberMap.get(m.projectId)?.push(m));
      
      return projectIds.map(id => memberMap.get(id) ?? []);
    }),
    
    userLoader: new DataLoader(async (userIds: readonly string[]) => {
      const users = await prisma.user.findMany({
        where: { id: { in: userIds as string[] } },
      });
      
      const userMap = new Map(users.map(u => [u.id, u]));
      return userIds.map(id => userMap.get(id) ?? null);
    }),
  };
}

// Resolvers
export const projectResolvers = {
  Project: {
    members: (parent: Project, _args: unknown, context: Context) => {
      return context.loaders.projectMembersLoader.load(parent.id);
    },
  },
  Task: {
    assignee: (parent: Task, _args: unknown, context: Context) => {
      if (!parent.assigneeId) return null;
      return context.loaders.userLoader.load(parent.assigneeId);
    },
  },
};

DataLoader batches all calls within a single tick of the event loop. 20 Project.members resolver calls → 1 batched findMany. The loader is created per-request (in context factory), not globally — different requests should have isolated caches.

Context and Auth

Write the context factory for Apollo Server.
Extract the JWT, verify it, and add the user + loaders to context.
Return null user if unauthenticated (don't throw — let resolvers decide auth requirements).
import { ApolloServer } from '@apollo/server';

export interface Context {
  user: AuthenticatedUser | null;
  loaders: ReturnType<typeof createLoaders>;
  prisma: PrismaClient;
}

const server = new ApolloServer<Context>({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }): Promise<Context> => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    let user: AuthenticatedUser | null = null;
    if (token) {
      try {
        const payload = verifyJwt(token);
        user = { id: payload.sub, email: payload.email };
      } catch {
        // Invalid token — resolve as unauthenticated
      }
    }
    
    return {
      user,
      loaders: createLoaders(prisma),
      prisma,
    };
  },
});

Resolver-Level Auth

Mutations require authentication. Some project fields require 
membership. Write a clean pattern for this.
import { GraphQLError } from 'graphql';

// Utility — throws GraphQL-formatted auth error
function requireAuth(context: Context): AuthenticatedUser {
  if (!context.user) {
    throw new GraphQLError('Authentication required', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
  return context.user;
}

export const mutationResolvers = {
  Mutation: {
    createTask: async (_parent: unknown, { input }: CreateTaskArgs, context: Context) => {
      const user = requireAuth(context);
      
      // Check project membership
      const member = await context.prisma.projectMember.findUnique({
        where: { userId_projectId: { userId: user.id, projectId: input.projectId } },
      });
      
      if (!member || member.role === 'VIEWER') {
        throw new GraphQLError('Insufficient permissions', {
          extensions: { code: 'FORBIDDEN' },
        });
      }
      
      return context.prisma.task.create({ data: { ...input, createdById: user.id } });
    },
  },
};

Subscriptions

Add a subscription for real-time task updates within a project.
Only send updates to members of the project.
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

export const subscriptionResolvers = {
  Subscription: {
    taskUpdated: {
      subscribe: async (_parent: unknown, { projectId }: { projectId: string }, context: Context) => {
        const user = requireAuth(context);
        
        // Verify membership before subscribing
        const isMember = await context.prisma.projectMember.count({
          where: { projectId, userId: user.id },
        });
        
        if (!isMember) {
          throw new GraphQLError('Not a project member', {
            extensions: { code: 'FORBIDDEN' },
          });
        }
        
        return pubsub.asyncIterator([`TASK_UPDATED_${projectId}`]);
      },
      resolve: (payload: { taskUpdated: Task }) => payload.taskUpdated,
    },
  },
};

// In mutation resolver, after update:
await pubsub.publish(`TASK_UPDATED_${task.projectId}`, { taskUpdated: updatedTask });

For production, replace graphql-subscriptions (in-memory) with Redis pub/sub for multi-instance deployments.

TypeScript + Code Generation

Set up graphql-codegen to generate TypeScript types from the schema.
Generate: resolver types, operation types for the frontend, 
and a typed Apollo Client.

Claude generates the codegen.ts config:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: './src/schema.graphql',
  documents: ['./src/**/*.graphql', './src/**/*.tsx'],
  generates: {
    // Server: typed resolvers
    './src/generated/resolvers.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        contextType: '../types#Context',
        mappers: {
          Project: '../db/types#DbProject',
          Task: '../db/types#DbTask',
        },
      },
    },
    // Client: typed operations
    './src/generated/operations.ts': {
      plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
    },
  },
};

export default config;

With generated resolver types, TypeScript catches missing resolvers and mismatched return types at compile time. The client operations file gives you typed query results from useQuery(GetProjectDocument).

Testing GraphQL APIs

Write integration tests for the createTask mutation.
Cover: success, unauthenticated, forbidden (viewer role), 
project not found.

Claude writes tests using ApolloServer.executeOperation (Apollo’s built-in testing method) — no HTTP server needed for GraphQL unit tests. It generates the exact mutation string, creates test context with mock user, and asserts on the union type results.

GraphQL with Claude Code

GraphQL schema design benefits especially from Claude Code because the schema is the spec — when you describe the behavior you want, Claude generates a schema that models it correctly, then generates the resolvers that implement it. The type system means errors surface as TypeScript compile errors rather than runtime surprises.

For the N+1 problem specifically: paste your query, your resolver code, and the number of queries you’re seeing in logs. Claude reads both the schema and implementation to identify exactly where the DataLoader is missing.

For testing patterns with GraphQL see the testing guide. For API design patterns that apply across REST and GraphQL see the code review guide — the security review section covers GraphQL-specific concerns like introspection in production and query depth limiting. The Claude Skills 360 bundle includes GraphQL skill sets covering schema design, DataLoader patterns, and subscription infrastructure. Start with the free tier.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free