Skip to content
Community content. Review instructions before giving them to an AI agent — treat modules like open-source code.

GraphQL Conventions

Design GraphQL APIs that scale: schema-first modeling, deliberate nullability, connection-style pagination, DataLoader batching, structured errors, and query depth/complexity limits.

Mby @markdownersPublished August 21, 2026 · ~3 min read

0 downloads · Used by 0 stacks

The schema is the API's public contract, not an implementation detail generated from resolvers — design it deliberately, in a schema-first process, before writing a single resolver.

Schema-first design

  • Write the SDL (types, fields, arguments) before implementing resolvers, and treat schema changes as API design decisions requiring the same review as a REST endpoint change — a schema generated as a byproduct of resolver code tends to leak implementation shape (internal field names, DB column types) into the public contract.
  • Name types and fields for what they mean to the consumer, not for the underlying table or service that backs them — a User type backed by three internal services should still read as one coherent User.
  • Use input types (input UpdateUserInput) for mutation arguments instead of long flat argument lists — it groups related fields, and adding a field to an input type is non-breaking while adding a bare argument to every call site is not.

Nullability

  • Default every field to nullable unless you can guarantee it will always resolve — a non-null field that fails to resolve (a downstream service error, a missing relation) takes down the entire parent object under GraphQL's null-propagation rules, not just that one field.
  • Make a field non-null only for values that are structurally guaranteed (an object's own id), never for values that depend on a network call, another service, or optional data.
  • Treat loosening non-null to nullable as a breaking change for existing clients (they may not have null-checked), even though technically it's "less strict" — communicate and version it like any other breaking change.

Pagination

  • Use the Relay-style connection pattern (edges, node, cursor, pageInfo { hasNextPage, endCursor }) for any list that can grow — it standardizes cursor-based pagination across the whole schema instead of each list type inventing its own shape.
  • Never expose raw offset/limit pagination on a list backed by data that can be inserted or deleted between page requests — same skip/duplicate problem as REST offset pagination, and connections solve it once for the whole schema.

The N+1 problem

  • Batch every resolver that loads related entities (a User.posts resolver called once per user in a list) through a DataLoader (or equivalent per-request batching cache) — without it, a list of N parents triggers N+1 individual queries, and this is the single most common GraphQL performance bug.
  • Scope the DataLoader instance to a single request, never share it across requests — a cross-request cache leaks data between users and serves stale results.

Error handling

  • Distinguish request-level errors (malformed query, auth failure — surfaced in the top-level errors array with no data) from field-level errors (one resolver failed but siblings succeeded — surfaced as null for that field plus an entry in errors with a path) — collapsing both into one error shape loses information the client needs to render partial results correctly.
  • Put a stable machine-readable error code in extensions (e.g. extensions: { code: "FORBIDDEN" }) rather than relying on clients to string-match the human-readable message.

Depth and complexity limits

  • Enforce a maximum query depth and a query complexity/cost limit (weighting fields and list multipliers) before executing any query — an unbounded nested query (user { friends { friends { friends { ... } } } }) is a trivial denial-of-service vector against an otherwise well-built API.
  • Reject over-limit queries at parse/validation time, before touching the database — validating cost after execution has already spent the resources you were trying to protect.

Mutations

  • Name mutations as verb-first, specific actions (createOrder, cancelSubscription), never a generic updateEntity(type, id, fields) — specific mutations let the schema express exactly what changed and what fields the response should return.
  • Return the affected object (or a payload wrapping it, e.g. { order: Order, errors: [UserError] }) from every mutation — a client that just performed a write needs the resulting state without a follow-up query.
Badge

Link back to this module from your own README.

Get it on Markdowners
[![Get it on Markdowners](https://markdowners.com/mdstack-badge.svg)](https://markdowners.com/m/markdowners/graphql-conventions)

Comments (0)

Sign in to comment. Sign in

No comments yet. Be the first to add one.

Discussions about this module

No discussions about this module yet.

Start a discussion