Generate GraphQL Schemas with Claude (2026)

The Problem

Designing a GraphQL schema requires careful thought about types, queries, mutations, input types, pagination, error handling, and naming conventions. Getting the schema wrong early leads to painful breaking changes later because GraphQL clients depend on exact field names and types.

Quick Start

Describe your data model and ask Claude Code to generate the schema:

Generate a GraphQL schema for a task management API.
Entities: User, Project, Task, Comment.
Include:
- Queries with pagination (cursor-based)
- Mutations for CRUD operations
- Input types with validation descriptions
- Proper nullability (use ! where values are guaranteed)
- Relay-style connection types for lists
Use the code-first approach with TypeGraphQL and TypeScript.

What’s Happening

GraphQL schemas define the contract between client and server. Unlike REST, where the API shape is implicit, GraphQL schemas are explicit and self-documenting. Every query, mutation, type, and field must be declared in the schema.

Claude Code excels at schema generation because it understands GraphQL conventions (Relay connections, input types, error unions), can derive the schema from your existing database models, and produces consistent naming throughout.

Step-by-Step Guide

Step 1: Choose your approach

Schema-first (write the schema, generate code):

# schema.graphql
type User {
 id: ID!
 email: String!
 name: String!
 projects: [Project!]!
 createdAt: DateTime!
}

Code-first (write TypeScript, generate schema):

// Using TypeGraphQL
@ObjectType()
class User {
 @Field(() => ID)
 id: string;
 @Field()
 email: string;
 @Field()
 name: string;
 @Field(() => [Project])
 projects: Project[];
}

Ask Claude Code which approach fits your project:

I have an existing Prisma schema with 12 models. Should I use
schema-first or code-first for my GraphQL API? I want type safety
between the database models and the GraphQL types.

Step 2: Generate types from your data model

Ask Claude Code to read your Prisma schema (or database models) and generate GraphQL types:

Read prisma/schema.prisma and generate GraphQL type definitions
for every model. Follow these conventions:
- Use Relay-style connections for list fields
- Make IDs non-nullable
- Include timestamps (createdAt, updatedAt)
- Exclude internal fields (passwordHash, deletedAt)
- Use proper GraphQL scalar types (DateTime, JSON)

Claude Code generates:

scalar DateTime
type User {
 id: ID!
 email: String!
 name: String!
 role: UserRole!
 projects: ProjectConnection!
 tasks(status: TaskStatus): TaskConnection!
 createdAt: DateTime!
 updatedAt: DateTime!
}
enum UserRole {
 ADMIN
 MEMBER
 VIEWER
}
type Project {
 id: ID!
 name: String!
 description: String
 owner: User!
 tasks(first: Int, after: String, status: TaskStatus): TaskConnection!
 members: [User!]!
 createdAt: DateTime!
 updatedAt: DateTime!
}
type Task {
 id: ID!
 title: String!
 description: String
 status: TaskStatus!
 priority: TaskPriority!
 assignee: User
 project: Project!
 comments(first: Int, after: String): CommentConnection!
 dueDate: DateTime
 createdAt: DateTime!
 updatedAt: DateTime!
}
enum TaskStatus {
 TODO
 IN_PROGRESS
 IN_REVIEW
 DONE
}
enum TaskPriority {
 LOW
 MEDIUM
 HIGH
 URGENT
}
type Comment {
 id: ID!
 body: String!
 author: User!
 task: Task!
 createdAt: DateTime!
 updatedAt: DateTime!
}

Step 3: Generate connection types for pagination

Relay-style pagination is the GraphQL standard:

type TaskConnection {
 edges: [TaskEdge!]!
 pageInfo: PageInfo!
 totalCount: Int!
}
type TaskEdge {
 node: Task!
 cursor: String!
}
type PageInfo {
 hasNextPage: Boolean!
 hasPreviousPage: Boolean!
 startCursor: String
 endCursor: String
}

Ask Claude Code to generate these for every list field:

Generate Relay-style connection types for all list relationships
in the schema. Include totalCount on every connection.

Step 4: Generate queries and mutations

type Query {
 # Single resource lookups
 user(id: ID!): User
 project(id: ID!): Project
 task(id: ID!): Task
 # List queries with pagination and filtering
 users(first: Int, after: String, search: String): UserConnection!
 projects(first: Int, after: String, ownerId: ID): ProjectConnection!
 tasks(
 first: Int
 after: String
 projectId: ID
 status: TaskStatus
 assigneeId: ID
 priority: TaskPriority
 ): TaskConnection!
 # Current user
 me: User!
}
type Mutation {
 # User mutations
 updateProfile(input: UpdateProfileInput!): User!
 # Project mutations
 createProject(input: CreateProjectInput!): Project!
 updateProject(id: ID!, input: UpdateProjectInput!): Project!
 deleteProject(id: ID!): Boolean!
 addProjectMember(projectId: ID!, userId: ID!, role: UserRole!): Project!
 # Task mutations
 createTask(input: CreateTaskInput!): Task!
 updateTask(id: ID!, input: UpdateTaskInput!): Task!
 deleteTask(id: ID!): Boolean!
 assignTask(taskId: ID!, userId: ID): Task!
 updateTaskStatus(taskId: ID!, status: TaskStatus!): Task!
 # Comment mutations
 addComment(input: AddCommentInput!): Comment!
 deleteComment(id: ID!): Boolean!
}

Step 5: Generate input types

input CreateProjectInput {
 name: String!
 description: String
}
input UpdateProjectInput {
 name: String
 description: String
}
input CreateTaskInput {
 projectId: ID!
 title: String!
 description: String
 priority: TaskPriority = MEDIUM
 assigneeId: ID
 dueDate: DateTime
}
input UpdateTaskInput {
 title: String
 description: String
 priority: TaskPriority
 assigneeId: ID
 dueDate: DateTime
}
input AddCommentInput {
 taskId: ID!
 body: String!
}
input UpdateProfileInput {
 name: String
 email: String
}

Step 6: Generate resolvers

Ask Claude Code to generate type-safe resolvers:

Generate Fastify/Mercurius resolvers for the GraphQL schema.
Use the Prisma client for data access. Include:
- DataLoader for N+1 prevention
- Authorization checks
- Input validation
- Error handling
// src/graphql/resolvers/task.ts
import { GraphQLContext } from '../context';
import DataLoader from 'dataloader';
export const taskResolvers = {
 Query: {
 task: async (_: unknown, { id }: { id: string }, ctx: GraphQLContext) => {
 ctx.requireAuth();
 const task = await ctx.prisma.task.findUnique({ where: { id } });
 if (!task) throw new Error('Task not found');
 return task;
 },
 tasks: async (_: unknown, args: TasksArgs, ctx: GraphQLContext) => {
 ctx.requireAuth();
 const { first = 20, after, projectId, status, assigneeId } = args;
 const where = {
 ...(projectId && { projectId }),
 ...(status && { status }),
 ...(assigneeId && { assigneeId }),
 deletedAt: null,
 };
 const cursor = after ? { id: after } : undefined;
 const tasks = await ctx.prisma.task.findMany({
 where,
 take: first + 1,
 cursor,
 skip: cursor ? 1 : 0,
 orderBy: { createdAt: 'desc' },
 });
 const hasNextPage = tasks.length > first;
 const edges = tasks.slice(0, first).map((task) => ({
 node: task,
 cursor: task.id,
 }));
 return {
 edges,
 pageInfo: {
 hasNextPage,
 hasPreviousPage: !!after,
 startCursor: edges[0]?.cursor ?? null,
 endCursor: edges[edges.length - 1]?.cursor ?? null,
 },
 totalCount: await ctx.prisma.task.count({ where }),
 };
 },
 },
 Task: {
 assignee: (task: { assigneeId: string | null }, _: unknown, ctx: GraphQLContext) => {
 if (!task.assigneeId) return null;
 return ctx.loaders.user.load(task.assigneeId);
 },
 project: (task: { projectId: string }, _: unknown, ctx: GraphQLContext) => {
 return ctx.loaders.project.load(task.projectId);
 },
 comments: async (task: { id: string }, args: PaginationArgs, ctx: GraphQLContext) => {
 // Paginated sub-query
 const { first = 20, after } = args;
 const comments = await ctx.prisma.comment.findMany({
 where: { taskId: task.id },
 take: first + 1,
 cursor: after ? { id: after } : undefined,
 skip: after ? 1 : 0,
 orderBy: { createdAt: 'asc' },
 });
 const hasNextPage = comments.length > first;
 const edges = comments.slice(0, first).map((c) => ({
 node: c,
 cursor: c.id,
 }));
 return {
 edges,
 pageInfo: { hasNextPage, hasPreviousPage: !!after },
 totalCount: await ctx.prisma.comment.count({ where: { taskId: task.id } }),
 };
 },
 },
};

Step 7: Generate DataLoaders for N+1 prevention

Generate DataLoader instances for all relationships that could
cause N+1 queries. Register them in the GraphQL context so they
are created fresh per request.
// src/graphql/loaders.ts
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
export function createLoaders(prisma: PrismaClient) {
 return {
 user: new DataLoader<string, User>(async (ids) => {
 const users = await prisma.user.findMany({
 where: { id: { in: [...ids] } },
 });
 const userMap = new Map(users.map((u) => [u.id, u]));
 return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
 }),
 project: new DataLoader<string, Project>(async (ids) => {
 const projects = await prisma.project.findMany({
 where: { id: { in: [...ids] } },
 });
 const projectMap = new Map(projects.map((p) => [p.id, p]));
 return ids.map((id) => projectMap.get(id) ?? new Error(`Project ${id} not found`));
 }),
 };
}

Prevention

Add GraphQL conventions to your CLAUDE.md:

## GraphQL Rules
- Use Relay-style connections for all list fields
- Every mutation input must be a dedicated Input type
- Use enums for finite value sets
- Non-nullable (!) by default, nullable only when data is absent
- Include totalCount on all connections
- Use DataLoader for all relationship resolvers
- Never expose internal IDs or sensitive fields

**Written by Michael** — solo dev, Da Nang, Vietnam. 50K+ Chrome extension users. $500K+ on Upwork (100% Job Success). Runs 5 Claude Max subs in parallel. Built this site with autonomous agent fleets. [See what I'm building →](https://zovo.one)

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=claude-code-graphql-schema-generation-guide)** $99 once. Free forever. 47/500 founding spots left.