| name | graphql-schema |
| description | Design GraphQL schema with types, queries, mutations, subscriptions. Outputs schema definition, resolvers, N+1 prevention, and error handling. |
| argument-hint | ["data model","query requirements","real-time needs"] |
| allowed-tools | Read, Write, Bash |
GraphQL Schema Design
Design production GraphQL API with types, queries, mutations, proper error handling, and N+1 query prevention. Not basic schema — pagination, authorization, batching, subscriptions.
Process
- Define types. Objects, scalars, enums, interfaces, unions.
- Design queries. Read operations with filtering, sorting, pagination.
- Create mutations. Write operations with input validation, errors.
- Add subscriptions. Real-time updates for WebSocket clients.
- Prevent N+1. DataLoader for batching, caching.
- Handle authorization. Field-level permissions, context.
- Plan error handling. Structured errors, codes, validation.
Output Format
GraphQL API: [Application Name]
Types: 15 object types
Queries: 20 read operations
Mutations: 12 write operations
Subscriptions: 3 real-time channels
N+1 Prevention: DataLoader batching
Schema Definition (SDL)
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
published: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id ID
String
User
Post
DateTime
user ID User
users
Int
String
UserFilter
UserConnection
post ID Post
posts
Int
String
Boolean
ID
PostConnection
createUser CreateUserInput CreateUserPayload
updateUser ID, UpdateUserInput UpdateUserPayload
deleteUser ID DeleteUserPayload
createPost CreatePostInput CreatePostPayload
publishPost ID PublishPostPayload
Post
commentAdded ID Comment
CreateUserInput
String
String
String
UpdateUserInput
String
String
CreatePostInput
String
String
Boolean
UserFilter
String
String
UserConnection
UserEdge
PageInfo
Int
UserEdge
User
String
PageInfo
Boolean
Boolean
String
String
CreateUserPayload
User
UserError
UserError
String
String
String
DateTime
JSON
Resolvers (Node.js + TypeScript)
import { GraphQLResolverMap } from '@apollo/server';
import DataLoader from 'dataloader';
const userResolvers: GraphQLResolverMap = {
Query: {
user: async (_, { id }, { db }) => {
return await db.user.findUnique({ where: { id } });
},
users: async (_, { first, after, filter }, { db }) => {
const cursor = after ? { id: after } : undefined;
const users = await db.user.findMany({
take: first + 1,
cursor,
where: {
...(filter?.email && { email: filter.email }),
...(filter?.nameContains && {
name: { contains: filter.nameContains }
})
},
orderBy: { createdAt: 'desc' }
});
const hasNextPage = users.length > first;
const nodes = hasNextPage ? users.slice(0, -1) : users;
{
: nodes.( ({
: user,
: user.
})),
: {
hasNextPage,
: !!after,
: nodes[]?.,
: nodes[nodes. - ]?.
},
: db..({ where })
};
}
},
: {
: (_, { input }, { db }) => {
{
(!input..()) {
{
: ,
: [{
: ,
: ,
:
}]
};
}
existing = db..({
: { : input. }
});
(existing) {
{
: ,
: [{
: ,
: ,
:
}]
};
}
user = db..({
: {
: input.,
: input.,
: (input.)
}
});
{ user, : [] };
} (error) {
{
: ,
: [{
: ,
: ,
:
}]
};
}
}
},
: {
: (user, _, { loaders }) => {
loaders..(user.);
}
}
};
DataLoader (N+1 Prevention)
import DataLoader from 'dataloader';
export function createLoaders(db: PrismaClient) {
return {
userById: new DataLoader(async (ids: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...ids] } }
});
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) || null);
}),
postsByAuthor: new DataLoader(async (authorIds: readonly string[]) => {
const posts = await db.post.({
: { : { : [...authorIds] } }
});
postsByAuthor = <, []>();
( post posts) {
existing = postsByAuthor.(post.) || [];
postsByAuthor.(post., [...existing, post]);
}
authorIds.( postsByAuthor.(id) || []);
})
};
}
server = ({
typeDefs,
resolvers,
: ({
: prisma,
: req.,
: (prisma)
})
});
Before DataLoader (N+1):
Query: users { posts { title } }
SQL queries:
1. SELECT * FROM users
2. SELECT * FROM posts WHERE author_id = 1 (user 1's posts)
3. SELECT * FROM posts WHERE author_id = 2 (user 2's posts)
... 100 queries for 100 users
After DataLoader (Batched):
SQL queries:
1. SELECT * FROM users
2. SELECT * FROM posts WHERE author_id IN (1,2,3,...100) (batched!)
Authorization
Field-Level Authorization
const resolvers = {
User: {
email: (user, _, { user: currentUser }) => {
if (currentUser.id === user.id || currentUser.role === 'admin') {
return user.email;
}
return null;
}
},
Query: {
user: async (_, { id }, { user, db }) => {
if (!user) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHORIZED' }
});
}
return await db.user.findUnique({ where: { id } });
}
}
};
Directive-Based Authorization
directive @auth(requires: Role = USER) on FIELD_DEFINITION | OBJECT
enum Role {
USER
ADMIN
}
type Query {
user(id: ID!): User @auth(requires: USER)
users: [User!]! @auth(requires: ADMIN)
}
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { requires } = this.args;
field.resolve = async (source, args, context, info) => {
if (!context.user) {
throw new GraphQLError('Unauthorized');
}
if (requires === 'ADMIN' && context.user.role !== 'admin') {
throw new GraphQLError('Forbidden');
}
return resolve(source, args, context, info);
};
}
}
Subscriptions (Real-time)
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Mutation: {
publishPost: async (_, { id }, { db }) => {
const post = await db.post.update({
where: { id },
data: { published: true }
});
pubsub.publish('POST_PUBLISHED', { postPublished: post });
return { post };
},
createComment: async (_, { input }, { db }) => {
const comment = await db.comment.create({ data: input });
pubsub.publish(`COMMENT_ADDED_${input.postId}`, {
commentAdded: comment
});
return { comment };
}
},
Subscription: {
postPublished: {
subscribe: () => pubsub.asyncIterator(['POST_PUBLISHED'])
},
commentAdded: {
subscribe: (_, { postId }) => {
pubsub.([]);
}
}
}
};
subscription = gql`;
(subscription, { : { : } });
Error Handling
import { GraphQLError } from 'graphql';
class ValidationError extends GraphQLError {
constructor(message: string, field: string) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
field
}
});
}
}
class NotFoundError extends GraphQLError {
constructor(resource: string) {
super(`${resource} not found`, {
extensions: { code: 'NOT_FOUND' }
});
}
}
const resolvers = {
Query: {
post: async (_, { id }, { db }) => {
const post = await db.post.findUnique({ where: { id } });
if (!post) {
throw new NotFoundError('Post');
}
return post;
}
},
Mutation: {
: (_, { input }, { user, db }) => {
(!user) {
(, {
: { : }
});
}
(input.. < ) {
(, );
}
post = db..({
: { ...input, : user. }
});
{ post, : [] };
}
}
};
{
: [{
: ,
: {
: ,
:
}
}]
}
Performance Optimization
Query Complexity Limit
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000)
]
});
Depth Limiting
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(5)]
});
Caching
const server = new ApolloServer({
cache: new InMemoryLRUCache({
maxSize: 100 * 1024 * 1024,
ttl: 300
})
});
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
}
}
};
Rules
- Use cursor-based pagination (Relay spec) for large lists — offset pagination doesn't scale.
- DataLoader required for N+1 prevention — batches and caches database queries per request.
- Field-level authorization, not type-level — different users see different fields on same object.
- Structured error responses with codes — clients can handle errors programmatically.
- Query complexity/depth limits prevent abuse — malicious queries can overwhelm server.
- Fresh DataLoader instance per request — prevents data leakage between requests.
- Subscriptions use PubSub for real-time — WebSocket connections for live updates.
- Input validation in resolvers before DB — return validation errors in structured format.
- Custom scalars for DateTime, JSON — better type safety than String.
- Mutation payloads return object + errors — partial success handling, client doesn't need to guess.