Backend Development6 min read

GraphQL Server Design

Design GraphQL APIs: schema-first design, resolvers, DataLoader for N+1, persisted queries, subscriptions, and federation for distributed graphs.

graphqlapolloschema-designresolvers

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

  1. Design the schemaThe GraphQL schema is the API contract - good schema design reduces resolver complexity and client friction.
  2. Implement resolvers with contextResolvers bridge the schema to your data layer; context provides per-request state (auth, DB, services).
  3. Solve N+1 with DataLoaderWithout DataLoader, each resolver call triggers individual DB queries - fetching 100 users' orders = 100 queries.
  4. Implement subscriptionsSubscriptions push real-time updates to clients - chat messages, order status changes, live metrics.
  5. 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/dataloader
  • GraphQL Yoga: https://the-guild.dev/graphql/yoga-server

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.