Medium15 minDistributed Systems
UpdatedAug 6, 2026
Edit

GraphQL API Design

Question Variations

  • "How does GraphQL prevent over-fetching and under-fetching?"
  • "What causes the N+1 problem in GraphQL, and how would you fix it?"
  • "How would you paginate a large GraphQL connection?"
  • "Where should authorization be enforced in a GraphQL API?"

Why This Is Asked

This evaluates whether a candidate understands GraphQL as a typed API contract rather than merely a flexible query syntax. A strong answer balances client-selected data with server-side performance, authorization, schema evolution, and operational controls.

Key Concepts

  • Schema design: Model domain types, queries, mutations, inputs, and connections with clear ownership and nullability.
  • Resolver efficiency: Avoid the N+1 query problem with batching and per-request caching, while keeping resolvers independently testable.
  • Authorization: Enforce permissions in resolvers or domain services; client-selected fields must not bypass access rules.
  • Query controls: Limit depth, complexity, page size, and execution time to protect backend services from expensive queries.

Question Variations

  • “How does GraphQL prevent over-fetching and under-fetching?”
  • “What causes the N+1 problem in GraphQL, and how would you fix it?”
  • “How would you paginate a large GraphQL connection?”
  • “Where should authorization be enforced in a GraphQL API?”

Answers by Technology

+ Add Variant
System DesignImprove this answer ✏️

Expected Answer

GraphQL provides a typed graph-shaped contract in which clients request precisely the fields they need. Design the schema around business concepts rather than mirroring database tables: use query fields for reads, mutations for state changes, input types for commands, and connection types for paginated lists. Nullability is part of the contract: mark a field non-null only when the server can uphold that guarantee, because a resolver error propagates through non-null parents.

The key server-side concern is execution cost. A nested query can cause one database call per parent item—the N+1 problem. Request-scoped batch loaders collect keys and issue one query, while caching preserves result sharing within the request. Enforce authorization at the resolver or domain-service layer for every protected field and mutation. Put limits on query depth, field complexity, page size, and execution time; persisted queries can further reduce arbitrary query exposure. Evolve a schema by adding fields and deprecating old ones, then observe usage before removing them.

Why It Matters

GraphQL can simplify client development, but unconstrained nested queries can overload databases and downstream services. A carefully designed schema makes data needs explicit without making the API an unbounded query engine.

Example Code

import DataLoader from "dataloader";

const users = new DataLoader(async (ids: readonly string[]) => {
  const rows = await db.user.findMany({ where: { id: { in: [...ids] } } });
  const byId = new Map(rows.map((row) => [row.id, row]));
  return ids.map((id) => byId.get(id) ?? null);
});

export const resolvers = {
  Order: { customer: (order: { customerId: string }) => users.load(order.customerId) },
};

Common Mistakes

  • Writing a resolver that queries the database for every nested item: A list of 100 orders can become 101 database queries and collapse under normal traffic.
  • Authorizing only top-level queries: A protected nested field can leak data when clients select it through another allowed object.
  • Exposing unlimited connections: Large page sizes and deeply nested selections let one request consume disproportionate resources.

Follow-up Questions

  • Why is a DataLoader request-scoped? (Answer: It batches and caches only for one operation, avoiding stale or cross-user cached authorization results.)
  • How do schema deprecations work? (Answer: Mark the old field deprecated, offer a replacement, observe client usage, then remove it in a planned breaking change.)

Related Questions

References