GraphQL
Design production GraphQL APIs with schemas, operation budgets, resolver batching, partial failures, admission control, and observability.
What is GraphQL?
GraphQL is a typed language and execution model for asking an API for a specific shape of data. A client sends an operation containing fields and variables. The server checks that operation against a schema, runs the selected field resolvers, and returns an execution result whose response names mirror the selection.
GraphQL matters when several clients need different views over connected domain data. It gives clients flexibility inside a governed schema, but that flexibility moves important work to the server: authorization, bounded list sizes, query admission, resolver batching, nullability, compatible evolution, and field-level observability. Start with API design if schemas and transport contracts are new to you.
The core invariant is: the schema defines what can be requested, not what is cheap or permitted for every user. Validation, authorization, and demand control are separate decisions, and all three must pass before expensive work is allowed to fan out.
Follow an operation from document to result
A GraphQL request has a predictable lifecycle. Keeping each boundary explicit makes it easier to diagnose whether a failure belongs to the caller, the API policy, a resolver, or a downstream system.
1 Syntax
Parse the document
Turn the operation text into a structured document. Syntax errors stop the request before execution.
2 Schema
Validate the selection
Check fields, arguments, variables, fragments, and operation selection against the current type system.
3 Admission
Apply request policy
Authenticate the caller, enforce trusted-document or public-query rules, cap list arguments, and reject excessive depth, breadth, aliases, batches, or estimated cost.
4 Resolvers
Execute fields
Resolve the selected graph, authorize domain access, batch compatible data loads, and propagate deadlines to dependencies.
5 Response
Complete the result
Coerce values to declared types and return data, errors, or both. An execution error includes a path to the response position that failed.
The September 2025 GraphQL specification defines validation, execution, value completion, and response semantics. The official execution guide provides a shorter walkthrough of field resolvers and result construction.
Model a domain contract, not database tables
The schema is the client-facing graph. It should name stable domain concepts and workflows even when the underlying stores, service boundaries, or joins change.
Domain entities
Object types
Group fields around concepts such as Product, Account, or Order. Object fields may resolve from different systems without exposing those storage details.
Write intent
Input types
Describe structured mutation input separately from output. Validate business rules in the domain layer after GraphQL has validated the input shape.
Shared shape
Interfaces and unions
Represent polymorphic results while forcing clients to select fields valid for each possible runtime type.
Failure boundary
Nullability
Nullable fields can preserve partial data. A failed non-null field propagates null to the nearest nullable parent, potentially removing a larger result subtree.
Treat nullability as a product decision
- Use non-null when the field is required for the parent value to be useful and the service can reliably uphold that promise.
- Keep optional enrichment nullable so one weak dependency does not erase unrelated core data.
- Distinguish missing data, denied data, and failed resolution in error codes and client behavior without leaking sensitive server details.
- Test null propagation whenever a schema change tightens or relaxes a field contract.
Budget selection cost before resolver work begins
Depth alone does not describe GraphQL work. A shallow operation can repeat an expensive field through aliases, while nested lists can multiply the number of response values and backend reads. Assign service-owned weights, cap list arguments, and combine per-operation admission with per-client traffic budgets.
The lab uses transparent design weights rather than invented benchmark latency. Change the operation shape, page size, traffic, and resolver access pattern. Compare the schema cost limit with the backend call envelope; either can become the binding constraint.
Can this selection set enter the executor?
Loading the GraphQL operation model.
The official GraphQL security guide recommends layered demand control: trusted documents where the client set is known, pagination, depth and breadth limits, batch limits, rate limits, and query complexity analysis. No single limit covers all operation shapes.
Batch resolver access without crossing request boundaries
The N+1 pattern appears when one parent lookup returns N objects and a child resolver then performs another backend call for each object. GraphQL did not create the relationship, but field-by-field execution can make the repeated access easy to miss.
Use a request-scoped loader to collect compatible keys, fetch them together, and return results in key order. The cache should normally live only for one request because its values can depend on caller identity, authorization, locale, or transaction state.
The official GraphQL performance guide describes the N+1 pattern and batching. The maintained DataLoader reference implementation requires batch results to align with the requested keys and describes request-scoped memoization.
Keep batching honest
- Batch only keys that share the same backend, authorization context, and consistency requirement.
- Preserve input order and return one value or error for every requested key.
- Propagate cancellation and deadlines instead of allowing detached batch work to run after the operation is no longer useful.
- Trace operation name, field path, batch size, backend calls, and resolver duration; endpoint-level latency alone hides the fan-out.
Trace partial data, rejection, and null bubbling
GraphQL distinguishes request errors, which prevent execution, from execution errors, which happen while resolving or completing a response position. An execution result may contain both data and errors. Clients must inspect the body and error paths rather than treating transport success as proof that every selected field succeeded.
Select each scenario and inspect the active path. Compare a cost rejection before execution, a nullable enrichment failure, a non-null core-field failure, and a valid operation denied by domain authorization.
The specification's response section defines request and execution result behavior, including error paths and non-null propagation. Keep application error codes stable, mask internal details, and record the full cause in server-side telemetry.
Bound reads and make writes explicit
Paginate every potentially large list
Cursor pagination is a contract for ordering and continuation, not just a response wrapper. Give the list a maximum first or last, use a deterministic sort with a stable tie-breaker, and keep cursors opaque. Define what happens when rows are inserted, deleted, or become unauthorized between pages.
Design mutations around business outcomes
- Accept one structured input and return a payload containing the updated domain value plus stable, client-actionable errors where appropriate.
- Include an idempotency key when a retry after a timeout could repeat a payment, reservation, message publication, or other external effect.
- Authorize from trusted server context; never accept caller identity or ownership as authoritative mutation input.
- Execute top-level mutation fields serially as GraphQL specifies, but do not mistake that ordering for a transaction across services.
- Publish events with an outbox or another recoverable handoff when a committed state change must reach asynchronous consumers.
The official pagination guide explains cursor connections. For HTTP deployments, follow the current GraphQL over HTTP specification for methods, media types, status behavior, and request parameters instead of inventing transport semantics in resolvers.
Evolve the graph without silent client breakage
Additive changes are usually easier to adopt than removals, but an added field can still create cost, privacy, or ownership problems. Treat schema changes as reviewed API changes with usage evidence.
1 Domain
Assign ownership
Name the team responsible for field semantics, authorization, reliability, cost, and deprecation support.
2 Schema
Check compatibility
Run schema diff checks and validate known operations. Review nullability, enum changes, argument defaults, and possible-type changes deliberately.
3 Operations
Observe real usage
Track named or trusted documents and field use. Anonymous operation text makes impact analysis and incident response harder.
4 Lifecycle
Deprecate, migrate, remove
Publish a replacement, notify owners, measure remaining use, and remove only after the agreed support window and enforcement checks pass.
For a federated graph, apply the same discipline to subgraph composition, entity keys, field ownership, cross-subgraph fetches, and rollout order. A composed schema can be valid while one operation still creates an unsafe distributed call chain.
Choose GraphQL for the right boundary
Flexible graph reads
GraphQL
Fits product-facing APIs where several clients need related, typed views and the server team can operate schema governance, demand control, batching, and field-level telemetry.
Resource and protocol semantics
REST-style HTTP
Fits APIs that benefit from explicit resource URLs, standard HTTP caching and status semantics, simple operations, and broad tooling with little execution indirection.
Service-to-service RPC
gRPC
Fits strongly typed internal calls, generated clients, streaming, and explicit service methods where client-selected graph traversal is not the main requirement.
GraphQL can coexist with both: it may aggregate domain services behind a client-facing graph, while large files, webhooks, health checks, and operational endpoints keep purpose-built HTTP contracts. Choose from workload and ownership needs, not from a goal of replacing every API with one endpoint.
Run a production readiness review
- Contract: domain-oriented names, bounded lists, deliberate nullability, stable error codes, and an owner for every field.
- Admission: operation naming, trusted documents where appropriate, depth and breadth limits, alias and batch caps, cost weights, and per-client quotas.
- Correctness: resolver authorization, idempotent mutations, request-scoped loaders, deadlines, cancellation, and explicit partial-data behavior.
- Performance: measured field and backend latency, batch sizes, response bytes, compression, cache policy, and dependency saturation under peak operation mixes.
- Evolution: schema checks, operation validation, usage telemetry, deprecation windows, and coordinated federation rollout when applicable.
- Recovery: dependency timeout drills, replay-safe writes, masked client errors, trace correlation, and reconciliation for ambiguous side effects.
Run the example with Node.js to see a bounded operation admitted and a broad operation rejected before execution. Its weights are intentionally local to the example; a real service should derive weights and limits from its own traces and dependency budgets.