GraphQL API Design Skill — GraphQL Best Practices
Overview
This skill provides comprehensive guidance for designing and implementing GraphQL APIs, including schema design with SDL, resolver implementation, DataLoader patterns for N+1 prevention, subscriptions, Apollo Federation, error handling, authentication, authorization, pagination, and production best practices. Based on GraphQL specification, Apollo Server documentation, and production best practices.
When to Use
Use this skill when:
- Designing GraphQL schemas with SDL (Schema Definition Language)
- Implementing GraphQL type definitions (Object, Input, Interface, Union, Enum, Scalar)
- Creating GraphQL queries and mutations
- Setting up GraphQL subscriptions with WebSocket or Server-Sent Events
- Implementing resolvers (object, field, class-based)
- Preventing N+1 queries with DataLoader pattern
- Handling GraphQL errors with GraphQLError and formatted errors
- Validating input with custom directives and schema directives
- Authenticating requests with JWT and context
- Authorizing access with directives and middleware
- Versioning GraphQL schemas (URL, header, schema evolution)
- Implementing pagination with Relay spec and cursor-based approaches
- Adding filtering with input types and arguments
- Implementing sorting with enum and input types
- Building search with full-text and fuzzy matching
- Batching requests with Apollo Link Batch and schema directives
- Caching with HTTP cache and Redis integration
- Optimizing performance with persisted queries and response caching
- Analyzing query complexity with graphql-query-complexity
- Limiting query depth with graphql-depth-limit
- Configuring introspection for production environments
- Choosing between SDL-first and code-first approaches
- Implementing Apollo Federation for microservices
- Using Apollo Schema Stitching for legacy API integration
- Configuring Apollo Server, Express GraphQL, or GraphQL Yoga
- Setting up GraphQL Mesh for REST/GraphQL federation
- Establishing production best practices and monitoring
Do NOT use this skill when:
- Designing RESTful APIs (use api-design skill for REST endpoint patterns)
- Building database schemas (use database-design skill for table structures)
- Implementing frontend GraphQL clients (use frontend framework skills)
- Setting up CI/CD pipelines (use devops-pipeline skill)
- Containerizing GraphQL servers (use docker-containerization skill)
- Deploying to cloud infrastructure (use terraform-iac or kubernetes-orchestration skill)
- Writing application business logic (use domain-driven skill)
- Performing security audits (use security-auditor skill)
Why avoid: GraphQL is specifically for API query language and runtime. Use REST for simple CRUD, databases for data storage, and the right tools for infrastructure and deployment.
Core Concepts
Schema Design Approaches
| Approach | Best For | Tools |
|---|---|---|
| Schema-First (SDL) | Team collaboration, API contracts | GraphQL Code Generator, graphql-tag |
| Code-First | Rapid prototyping, TypeScript projects | type-graphql, Nexus, GraphQL Tools |
| Federation | Microservices, distributed teams | Apollo Gateway, Apollo Router |
| Schema Stitching | Legacy integration, gradual migration | Apollo Schema Stitching |
Type System Overview
| Type | Purpose | Example |
|---|---|---|
| Object | Complex data structures | type User { id: ID! name: String! } |
| Input | Mutation/Query input shapes | input CreateUserInput { name: String! } |
| Interface | Shared fields across types | interface Node { id: ID! } |
| Union | One of multiple types | union SearchResult = User | Post |
| Enum | Fixed set of values | enum Role { ADMIN USER } |
| Scalar | Custom primitive types | scalar DateTime, scalar JSON |
Server Framework Comparison
| Framework | Language | Key Features |
|---|---|---|
| Apollo Server | Node.js | Federation, caching, tracing, production-ready |
| GraphQL Yoga | Node.js | Built-in subscriptions, PWA support, lightweight |
| Express GraphQL | Node.js | Minimal, Express middleware integration |
| GraphQL Mesh | Node.js | REST/GraphQL federation, multi-source |
| Strawberry | Python | Python-native, async support, type inference |
| Graphene | Python | Django/Flask integration, schema-first |
Schema Design with SDL
Complete Schema Example
graphql# ============================================ # Scalar Types # ============================================ scalar DateTime scalar JSON scalar UUID scalar URL scalar Email # ============================================ # Enum Types # ============================================ enum Role { ADMIN MODERATOR USER GUEST } enum Status { ACTIVE INACTIVE PENDING SUSPENDED } enum SortOrder { ASC DESC } # ============================================ # Interface Types # ============================================ interface Node { id: ID! } interface Timestamps { createdAt: DateTime! updatedAt: DateTime! } # ============================================ # Union Types # ============================================ union SearchResult = User | Post | Comment union AuthenticationResult = LoginSuccess | LoginError # ============================================ # Input Types # ============================================ input CreateUserInput { name: String! email: Email! password: String! role: Role = USER } input UpdateUserInput { name: String email: Email role: Role } input UserFilterInput { status: Status role: Role search: String createdAtFrom: DateTime createdAtTo: DateTime } input UserSortInput { field: UserSortField = createdAt order: SortOrder = DESC } enum UserSortField { name email createdAt updatedAt } # ============================================ # Object Types # ============================================ type User implements Node & Timestamps { id: ID! name: String! email: Email! role: Role! status: Status! posts: [Post!]! comments: [Comment!]! createdAt: DateTime! updatedAt: DateTime! } type Post implements Node & Timestamps { id: ID! title: String! content: String! author: User! comments: [Comment!]! status: Status! createdAt: DateTime! updatedAt: DateTime! } type Comment implements Node & Timestamps { id: ID! text: String! author: User! post: Post! createdAt: DateTime! updatedAt: DateTime! } # ============================================ # Pagination Types (Relay Spec) # ============================================ type UserEdge { node: User! cursor: String! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } # ============================================ # Authentication Types # ============================================ type LoginSuccess { token: String! user: User! } type LoginError { message: String! code: String! } input LoginInput { email: Email! password: String! } # ============================================ # Query Type # ============================================ type Query { user(id: ID!): User post(id: ID!): Post users( first: Int = 20 after: String filter: UserFilterInput sort: UserSortInput ): UserConnection! search(query: String!): [SearchResult!]! health: HealthStatus! } type HealthStatus { status: String! version: String! timestamp: DateTime! } # ============================================ # Mutation Type # ============================================ type Mutation { login(input: LoginInput!): AuthenticationResult! logout: Boolean! createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean! createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! deletePost(id: ID!): Boolean! } # ============================================ # Subscription Type # ============================================ type Subscription { userCreated: User! userUpdated(id: ID!): User! postCreated(categoryId: ID): Post! commentAdded(postId: ID!): Comment! }
Resolver Implementation
Object Resolvers
typescriptimport { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'; import { GraphQLError } from 'graphql/error'; import { DateTimeResolver } from 'graphql-scalars'; import { User, Post, Comment } from '../types'; import { UserRole, UserStatus } from '../enums'; import { prisma } from '../db'; import { requireAuth, requireRole } from '../middleware'; // Custom Scalar Resolvers const DateTime: GraphQLScalarType = new DateTimeResolver(); // Object Type Resolvers const UserResolvers = { posts: (user: User) => prisma.post.findMany({ where: { authorId: user.id } }), comments: (user: User) => prisma.comment.findMany({ where: { authorId: user.id } }), fullName: (user: User) => `${user.firstName} ${user.lastName}`, ssn: (user: User, args, context) => { requireAuth(context); requireRole(context, UserRole.ADMIN); return user.ssn; }, }; const PostResolvers = { author: (post: Post) => prisma.user.findUnique({ where: { id: post.authorId } }), comments: (post: Post) => prisma.comment.findMany({ where: { postId: post.id } }), wordCount: (post: Post) => post.content.split(/\s+/).length, excerpt: (post: Post) => post.content.substring(0, 200) + '...', }; // Root Resolvers (Query, Mutation, Subscription) const resolvers = { Query: { user: (_, { id }, context) => { requireAuth(context); return prisma.user.findUnique({ where: { id } }); }, users: (_, { first = 20, after, filter, sort }, context) => { requireAuth(context); const where: any = {}; if (filter?.status) where.status = filter.status; if (filter?.role) where.role = filter.role; if (filter?.search) { where.OR = [ { name: { contains: filter.search } }, { email: { contains: filter.search } }, ]; } const orderBy: any = {}; const sortField = sort?.field || 'createdAt'; const sortOrder = sort?.order || 'DESC'; orderBy[sortField] = sortOrder; const cursor = after ? { id: after } : undefined; const take = first + 1; const users = await prisma.user.findMany({ where, orderBy, cursor, take, skip: after ? 1 : 0, }); const hasNextPage = users.length > first; const data = hasNextPage ? users.slice(0, -1) : users; return { edges: data.map((user) => ({ node: user, cursor: user.id })), pageInfo: { hasNextPage, hasPreviousPage: !!after, startCursor: data.length > 0 ? data[0].id : null, endCursor: data.length > 0 ? data[data.length - 1].id : null, }, totalCount: await prisma.user.count({ where }), }; }, search: async (_, { query }, context) => { requireAuth(context); const [users, posts, comments] = await Promise.all([ prisma.user.findMany({ where: { OR: [{ name: { contains: query } }, { email: { contains: query } }], }, take: 10, }), prisma.post.findMany({ where: { OR: [ { title: { contains: query } }, { content: { contains: query } }, ], }, take: 10, }), prisma.comment.findMany({ where: { text: { contains: query } }, take: 10, }), ]); return [...users, ...posts, ...comments]; }, health: () => ({ status: 'ok', version: process.env.APP_VERSION || '1.0.0', timestamp: new Date().toISOString(), }), }, Mutation: { login: async (_, { input }) => { const user = await prisma.user.findUnique({ where: { email: input.email }, }); if (!user || !(await bcrypt.compare(input.password, user.password))) { return { __typename: 'LoginError', message: 'Invalid credentials', code: 'AUTH_FAILED', }; } const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET); return { __typename: 'LoginSuccess', token, user }; }, createUser: (_, { input }, context) => { requireAuth(context); requireRole(context, UserRole.ADMIN); const hashedPassword = await bcrypt.hash(input.password, 10); return prisma.user.create({ data: { ...input, password: hashedPassword }, }); }, updateUser: async (_, { id, input }, context) => { requireAuth(context); if (context.userId !== id && context.role !== UserRole.ADMIN) { throw new GraphQLError('Unauthorized', { extensions: { code: 'FORBIDDEN' }, }); } return prisma.user.update({ where: { id }, data: input }); }, deleteUser: async (_, { id }, context) => { requireAuth(context); requireRole(context, UserRole.ADMIN); await prisma.user.delete({ where: { id } }); return true; }, }, Subscription: { userCreated: { subscribe: (_, __, context) => context.pubsub.asyncIterator(['USER_CREATED']), }, userUpdated: { subscribe: (_, { id }, context) => context.pubsub.asyncIterator([`USER_UPDATED:${id}`]), }, postCreated: { subscribe: (_, { categoryId }, context) => { const events = categoryId ? [`POST_CREATED:${categoryId}`] : ['POST_CREATED']; return context.pubsub.asyncIterator(events); }, }, commentAdded: { subscribe: (_, { postId }, context) => context.pubsub.asyncIterator([`COMMENT_ADDED:${postId}`]), }, }, User: UserResolvers, Post: PostResolvers, AuthenticationResult: { __resolveType(obj) { if (obj.token) return 'LoginSuccess'; return 'LoginError'; }, }, SearchResult: { __resolveType(obj) { if (obj.email) return 'User'; if (obj.title) return 'Post'; return 'Comment'; }, }, DateTime, }; export default resolvers;
DataLoader Pattern (N+1 Prevention)
DataLoader Implementation
typescriptimport DataLoader from 'dataloader'; import { prisma } from '../db'; // User DataLoader const createUserLoader = () => new DataLoader(async (userIds: readonly string[]) => { const users = await prisma.user.findMany({ where: { id: { in: Array.from(userIds) } }, }); return userIds.map((id) => users.find((u) => u.id === id) || null); }); // Post DataLoader const createPostLoader = () => new DataLoader(async (postIds: readonly string[]) => { const posts = await prisma.post.findMany({ where: { id: { in: Array.from(postIds) } }, }); return postIds.map((id) => posts.find((p) => p.id === id) || null); }); // Comment count DataLoader (with aggregation) const createCommentLoader = () => new DataLoader(async (postIds: readonly string[]) => { const comments = await prisma.comment.groupBy({ by: ['postId'], _count: { _all: true }, where: { postId: { in: Array.from(postIds) } }, }); return postIds.map( (postId) => comments.find((c) => c.postId === postId)?._count._all || 0 ); }); // Context setup (new DataLoader instances per request) const createContext = (req: Request) => ({ prisma, pubsub, userId: getUserIdFromToken(req), role: getRoleFromToken(req), userLoader: createUserLoader(), postLoader: createPostLoader(), commentLoader: createCommentLoader(), }); // Resolver using DataLoader const resolvers = { Post: { // ✅ GOOD: Uses DataLoader (1 query for N posts) author: (post, __, context) => context.userLoader.load(post.authorId), commentCount: (post, __, context) => context.commentLoader.load(post.id), // ❌ BAD: Causes N+1 queries (1 query per post) // author: (post) => prisma.user.findUnique({ where: { id: post.authorId } }) }, Comment: { author: (comment, __, context) => context.userLoader.load(comment.authorId), post: (comment, __, context) => context.postLoader.load(comment.postId), }, };
DataLoader Best Practices
| Practice | Description |
|---|---|
| Per-request instances | Create new DataLoader per request to avoid caching stale data |
| Batch factory functions | Use factory functions to create DataLoaders with fresh state |
| Cache key consistency | Ensure batch function returns results in same order as input keys |
| Null handling | Return null for missing keys, not undefined |
| Error handling | Use DataLoader's built-in error handling or maxBatchSize |
Error Handling
Custom Error Classes
typescriptimport { GraphQLError, GraphQLFormattedError } from 'graphql'; class UserInputError extends GraphQLError { constructor(message: string, properties?: Record<string, unknown>) { super(message, { extensions: { code: 'BAD_USER_INPUT', timestamp: new Date().toISOString(), ...properties, }, }); } } class AuthenticationError extends GraphQLError { constructor(message: string = 'Authentication required') { super(message, { extensions: { code: 'UNAUTHENTICATED', timestamp: new Date().toISOString(), }, }); } } class ForbiddenError extends GraphQLError { constructor(message: string = 'Insufficient permissions') { super(message, { extensions: { code: 'FORBIDDEN', timestamp: new Date().toISOString() }, }); } } class NotFoundError extends GraphQLError { constructor(message: string, properties?: Record<string, unknown>) { super(message, { extensions: { code: 'NOT_FOUND', timestamp: new Date().toISOString(), ...properties, }, }); } } // Format errors for production const formatError = (error: GraphQLFormattedError): GraphQLFormattedError => { const { code, timestamp, ...extensions } = error.extensions || {}; if ( process.env.NODE_ENV === 'production' && code === 'INTERNAL_SERVER_ERROR' ) { return { ...error, message: 'Internal server error', extensions: { code, timestamp }, }; } return { ...error, extensions: { code, timestamp, ...extensions } }; }; // Usage in resolvers const resolvers = { Query: { user: (_, { id }, context) => { if (!context.userId) throw new AuthenticationError(); const user = await prisma.user.findUnique({ where: { id } }); if (!user) throw new NotFoundError(`User with id ${id} not found`, { id }); return user; }, updateUser: async (_, { id, input }, context) => { if (!context.userId) throw new AuthenticationError(); if (input.email && !isValidEmail(input.email)) { throw new UserInputError('Invalid email format', { field: 'email' }); } if (context.userId !== id && context.role !== UserRole.ADMIN) { throw new ForbiddenError('Cannot update other users'); } return prisma.user.update({ where: { id }, data: input }); }, }, };
Authentication and Authorization
JWT Authentication with Context
typescriptimport jwt from 'jsonwebtoken'; interface Context { userId?: string; user?: User; role?: UserRole; prisma: PrismaClient; pubsub: PubSub; userLoader: DataLoader<string, User>; postLoader: DataLoader<string, Post>; } // JWT Verification const verifyToken = (token: string): { userId: string; role: UserRole } => { try { return jwt.verify(token, process.env.JWT_SECRET) as { userId: string; role: UserRole; }; } catch { throw new AuthenticationError('Invalid or expired token'); } }; // Context Creator const createContext = ({ req }: CreateContextOptions): Context => { const token = req.headers.authorization?.replace('Bearer ', ''); let userId: string | undefined; let role: UserRole | undefined; if (token) { const decoded = verifyToken(token); userId = decoded.userId; role = decoded.role; } return { userId, role, prisma, pubsub, userLoader: createUserLoader(), postLoader: createPostLoader(), }; }; // Middleware Functions const requireAuth = (context: Context): void => { if (!context.userId) throw new AuthenticationError(); }; const requireRole = (context: Context, ...roles: UserRole[]): void => { requireAuth(context); if (!roles.includes(context.role!)) throw new ForbiddenError('Insufficient permissions'); }; const requireOwner = (context: Context, resourceOwnerId: string): void => { requireAuth(context); if (context.role !== UserRole.ADMIN && context.userId !== resourceOwnerId) { throw new ForbiddenError('You can only modify your own resources'); } };
Schema Directives for Authorization
typescriptconst authDirective = { typeDefinition: `directive @auth(requires: Role = USER) on FIELD_DEFINITION`, resolverDefinition: ({ requires = UserRole.USER, }: { requires?: UserRole; }) => { return async (next, parent, args, context, info) => { requireAuth(context); requireRole(context, requires); return next(); }; }, }; // Usage in schema // type Query { // adminDashboard: DashboardStats! @auth(requires: ADMIN) // user: User! @auth(requires: USER) // }
Pagination Patterns
Relay Cursor-Based Pagination
typescript// Schema type Query { users( first: Int = 20 after: String last: Int before: String filter: UserFilterInput sort: UserSortInput ): UserConnection! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } // Resolver const resolvers = { Query: { users: async (_, { first = 20, after, last, before, filter, sort }, context) => { const where = buildWhere(filter); const orderBy = buildOrderBy(sort); // Forward pagination if (first !== undefined) { const cursor = after ? { id: after } : undefined; const take = first + 1; const users = await prisma.user.findMany({ where, orderBy, cursor, take, skip: after ? 1 : 0 }); const hasNextPage = users.length > first; const data = hasNextPage ? users.slice(0, -1) : users; return { edges: data.map(user => ({ node: user, cursor: encodeCursor(user.id) })), pageInfo: { hasNextPage, hasPreviousPage: !!after, startCursor: data.length > 0 ? encodeCursor(data[0].id) : null, endCursor: data.length > 0 ? encodeCursor(data[data.length - 1].id) : null }, totalCount: await prisma.user.count({ where }) }; } // Backward pagination if (last !== undefined) { const cursor = before ? { id: before } : undefined; const take = last + 1; const users = await prisma.user.findMany({ where, orderBy: reverseOrder(orderBy), cursor, take, skip: before ? 1 : 0 }); const hasPreviousPage = users.length > last; const data = hasPreviousPage ? users.slice(0, -1) : users; data.reverse(); return { edges: data.map(user => ({ node: user, cursor: encodeCursor(user.id) })), pageInfo: { hasNextPage: !!before, hasPreviousPage, startCursor: data.length > 0 ? encodeCursor(data[0].id) : null, endCursor: data.length > 0 ? encodeCursor(data[data.length - 1].id) : null }, totalCount: await prisma.user.count({ where }) }; } } } }; const encodeCursor = (id: string): string => Buffer.from(`cursor:${id}`).toString('base64'); const decodeCursor = (cursor: string): string => Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
Offset-Based Pagination (Alternative)
typescripttype Query { users( page: Int = 1 limit: Int = 20 filter: UserFilterInput sort: UserSortInput ): UserPagination! } type UserPagination { data: [User!]! total: Int! page: Int! limit: Int! totalPages: Int! hasNextPage: Boolean! hasPreviousPage: Boolean! } // Resolver const resolvers = { Query: { users: async (_, { page = 1, limit = 20, filter, sort }, context) => { const where = buildWhere(filter); const orderBy = buildOrderBy(sort); const skip = (page - 1) * limit; const [data, total] = await Promise.all([ prisma.user.findMany({ where, orderBy, skip, take: limit }), prisma.user.count({ where }) ]); const totalPages = Math.ceil(total / limit); return { data, total, page, limit, totalPages, hasNextPage: page < totalPages, hasPreviousPage: page > 1 }; } } };
Apollo Federation
Subgraph Setup
typescript// subgraph-user/src/index.ts import { ApolloServer } from '@apollo/server'; import { buildSubgraphSchema } from '@apollo/subgraph'; const server = new ApolloServer({ schema: buildSubgraphSchema({ typeDefs, resolvers }), }); const typeDefs = `#graphql extend type Query { _service: _Service! } type User implements @key(fields: "id") { id: ID! name: String! email: String! role: Role! } enum Role { ADMIN USER } `; const resolvers = { Query: { user: (_, { id }) => findUserById(id) }, User: { __resolveReference: ({ id }) => findUserById(id) }, };
Apollo Gateway Setup
typescript// gateway/src/index.ts import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloGateway } from '@apollo/gateway'; import express from 'express'; import { json } from 'body-parser'; const gateway = new ApolloGateway({ supergraphs: [ { url: 'http://localhost:4001', sdl: fs.readFileSync('./user-subgraph.schema', 'utf8'), }, { url: 'http://localhost:4002', sdl: fs.readFileSync('./post-subgraph.schema', 'utf8'), }, ], }); const server = new ApolloServer(); const app = express(); async function start() { const { schema } = await server.start(); app.use( '/graphql', json(), expressMiddleware(server, { context: async ({ req }) => ({ userId: getUserIdFromToken(req) }), }) ); await server.listen({ port: 4000 }); console.log('Gateway ready at http://localhost:4000/graphql'); } start();
Performance Optimization
Query Complexity Analysis
typescriptimport { createComplexityLimitRule } from 'graphql-validation-complexity'; const validationRules = [ createComplexityLimitRule(1000, { scalarCost: 1, objectCost: 0, listFactor: 10, }), ]; const server = new ApolloServer({ schema, validationRules, plugins: [ persistedQueries.plugin({ cache: new InMemoryLRUCache() }), responseCachePlugin({ ttl: { default: 60, Query: { users: 30, health: 300 }, }, }), ], });
Query Depth Limiting
typescriptimport depthLimit from 'graphql-depth-limit'; const validationRules = [ depthLimit(10), // Maximum query depth of 10 createComplexityLimitRule(1000), ]; const server = new ApolloServer({ schema, validationRules, formatError });
Server Configuration
Apollo Server Setup
typescriptimport { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { startStandaloneServer } from '@apollo/server/standalone'; import { json } from 'body-parser'; import cors from 'cors'; import { schema } from './schema'; import { createContext } from './context'; import { formatError } from './errors'; import { validationRules } from './validation'; const server = new ApolloServer({ schema, formatError, validationRules, introspection: process.env.NODE_ENV !== 'production', plugins: [ { async serverWillStart() { return { async drainServer() { await prisma.$disconnect(); }, }; }, }, ], }); // Standalone mode const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async (requestContext) => createContext(requestContext), }); console.log(`Server ready at ${url}`);
GraphQL Yoga Setup
typescriptimport { createYoga } from 'graphql-yoga'; import { schema } from './schema'; const yoga = createYoga({ schema, context: async ({ request }) => ({ userId: getUserIdFromToken(request), userLoader: createUserLoader(), }), graphqlEndpoint: '/graphql', ide: process.env.NODE_ENV !== 'production' ? 'graphiql' : 'apollo', cors: { origin: ['https://yourdomain.com'], credentials: true, }, }); server.listen(4000).then(({ url }) => { console.log(`Server ready at ${url}`); });
Production Checklist
Before deploying to production:
- Schema design follows GraphQL best practices
- DataLoader implemented for N+1 prevention
- Query complexity analysis configured
- Query depth limiting enabled
- Error handling with custom error classes
- Authentication with JWT and context
- Authorization with directives and middleware
- Pagination with Relay spec or cursor-based
- Subscriptions with WebSocket or SSE
- Introspection disabled in production
- Rate limiting configured
- Response caching enabled
- Persisted queries configured
- Health check endpoint implemented
- Logging and monitoring setup
- GraphQL Playground disabled in production
- CORS configured correctly
- Schema validation in CI/CD pipeline
Real-World Impact
Before this skill:
- N+1 query problems causing slow responses
- No authentication or authorization
- Missing error handling
- No pagination or filtering
- Deep queries causing server overload
- No performance monitoring
After this skill:
- Efficient data loading with DataLoader
- Secure authentication and authorization
- Comprehensive error handling
- Relay-compliant pagination
- Query complexity and depth limiting
- Production-ready monitoring
Cross-References
api-design- For RESTful API design patternsdatabase-design- For database schema designredis-caching- For Redis caching strategiesdocker-containerization- For containerizing GraphQL serverskubernetes-orchestration- For deploying to Kubernetesdevops-pipeline- For CI/CD with GraphQLsecurity-auditor- For security reviewsmicroservices-architecture- For Apollo Federation patterns


