GraphQL Server Design
Design GraphQL APIs: schema-first design, resolvers, DataLoader for N+1, persisted queries, subscriptions, and federation for distributed graphs.
GraphQL provides a query language for your API - clients request exactly the data they need. It's ideal for complex data graphs, mobile clients needing minimal payloads, and rapid frontend iteration. Production GraphQL requires solving N+1 queries, implementing authorization at the resolver level, and considering federation for large graphs.
Use GraphQL when: (1) you have diverse clients with different data needs, (2) you're building a BFF/API gateway, or (3) data fetching flexibility is a competitive advantage. REST is simpler for straightforward CRUD. Solve N+1 with DataLoader as a non-negotiable.
Workflow
- Design the schemaThe GraphQL schema is the API contract - good schema design reduces resolver complexity and client friction.
- Implement resolvers with contextResolvers bridge the schema to your data layer; context provides per-request state (auth, DB, services).
- Solve N+1 with DataLoaderWithout DataLoader, each resolver call triggers individual DB queries - fetching 100 users' orders = 100 queries.
- Implement subscriptionsSubscriptions push real-time updates to clients - chat messages, order status changes, live metrics.
- Secure resolvers with authorizationGraphQL's flexibility means any field can be queried - authorization must happen at the resolver level.
Output Format
SDL-first schema with connections for pagination, resolver per field with context (db + current user), DataLoader per entity type in context, subscriptions with Redis PubSub for scale, field-level authorization.
Examples
DataLoader batch: `const ordersByUserLoader = new DataLoader<string, Order[]>(async (userIds) => { const orders = await db.orders.findMany({ userId: { in: [...userIds] } }); const byUser = orders.reduce((map, order) => { (map[order.userId] ||= []).push(order); return map }, {} as Record<string, Order[]>); return userIds.map(id => byUser[id] ?? []) })`Auth directive: `const authDirectiveTransformer = (schema: GraphQLSchema) => mapSchema(schema, { [MapperKind.OBJECT_FIELD]: (fieldConfig) => { const auth = getDirective(schema, fieldConfig, "auth")?.[0]; if (auth) { const { resolve = defaultFieldResolver } = fieldConfig; fieldConfig.resolve = async (src, args, ctx, info) => { if (!ctx.currentUser) throw new GraphQLError("Not authenticated"); return resolve(src, args, ctx, info) } } return fieldConfig } })`Connection pattern: `type ProductConnection { edges: [ProductEdge!]! pageInfo: PageInfo! totalCount: Int! } type ProductEdge { node: Product! cursor: String! } type PageInfo { hasNextPage: Boolean! endCursor: String }`Edge Cases & Gotchas
- Query depth/complexity attacks: malicious queries can be infinitely nested. Add `graphql-depth-limit` (max 7 levels) and `graphql-cost-analysis` (budget per query) as middleware
- Introspection in production: disable GraphQL introspection in production for security: `introspection: process.env.NODE_ENV !== "production"`. It exposes your entire schema to attackers
- N+1 in mutations: DataLoader only batches within a single event loop tick. Mutations with side effects (sending emails per item) are not batched - be explicit about this in mutation design
When to Break These Rules
- For simple CRUD APIs or public-facing APIs consumed by unknown clients, REST is simpler, more cacheable (HTTP caching), and has better tooling support (OpenAPI). GraphQL shines for BFFs and developer platforms.
Reference Files
GraphQL spec: https://spec.graphql.org/Apollo Server: https://www.apollographql.com/docs/apollo-server/DataLoader: https://github.com/graphql/dataloaderGraphQL Yoga: https://the-guild.dev/graphql/yoga-server