| name | graphql-patterns |
| description | Schema design, resolver patterns, DataLoader, N+1 prevention, and subscription patterns for GraphQL APIs. |
GraphQL Patterns
Production-grade GraphQL API design with performance and type safety.
Schema Design Principles
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
email: String!
displayName: String!
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreatePostInput {
title: String!
body: String!
tags: [String!]
}
type CreatePostSuccess {
post: Post!
}
type ValidationError {
field: String!
message: String!
}
union CreatePostResult = CreatePostSuccess | ValidationError
DataLoader - N+1 Prevention
import DataLoader from 'dataloader'
function createUserLoader(db: Database) {
return new DataLoader<string, User | null>(async (userIds) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } }
})
const userMap = new Map(users.map(u => [u.id, u]))
return userIds.map(id => userMap.get(id) ?? null)
})
}
function createContext(req: Request) {
const db = getDatabase()
return {
db,
loaders: {
user: createUserLoader(db),
: (db),
: (db),
}
}
}
resolvers = {
: {
: {
ctx...(post.)
}
}
}
Resolver Pattern with Validation
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(10).max(50000),
tags: z.array(z.string()).max(10).optional()
})
const resolvers = {
Mutation: {
createPost: async (_parent: unknown, args: { input: unknown }, ctx: Context) => {
if (!ctx.currentUser) {
throw new AuthenticationError('Login required')
}
const parsed = CreatePostSchema.safeParse(args.input)
if (!parsed.success) {
return {
__typename: 'ValidationError',
: parsed..[]..(),
: parsed..[].
}
}
post = ctx...({
: { ...parsed., : ctx.. }
})
{ : , post }
}
}
}
Subscription Patterns
import { PubSub, withFilter } from 'graphql-subscriptions'
const pubsub = new PubSub()
const EVENTS = {
POST_CREATED: 'POST_CREATED',
COMMENT_ADDED: 'COMMENT_ADDED',
} as const
const resolvers = {
Subscription: {
commentAdded: {
subscribe: withFilter(
() => pubsub.asyncIterableIterator(EVENTS.COMMENT_ADDED),
(payload, variables) => payload.commentAdded.postId === variables.postId
)
}
},
Mutation: {
addComment: async (_p: unknown, args: { postId: string; body: string }, ctx: Context) => {
const comment = await ctx.db.comment.create({
data: { postId: args., : args., : ctx.!. }
})
pubsub.(., { : comment })
comment
}
}
}
Query Depth & Complexity Limiting
import depthLimit from 'graphql-depth-limit'
import { createComplexityLimitRule } from 'graphql-validation-complexity'
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(7),
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
})
]
})
Checklist
Anti-Patterns
- Exposing database IDs directly (use opaque/global IDs)
- Resolver doing N+1 queries without DataLoader
- Sharing DataLoader instances across requests (stale data, auth leak)
- Offset pagination on large datasets (performance cliff)
- God queries: single resolver fetching entire object graph
- Putting business logic in resolvers (keep resolvers thin, use service layer)