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
Usertype backed by three internal services should still read as one coherentUser. - 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.postsresolver 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
errorsarray with nodata) from field-level errors (one resolver failed but siblings succeeded — surfaced asnullfor that field plus an entry inerrorswith apath) — collapsing both into one error shape loses information the client needs to render partial results correctly. - Put a stable machine-readable error
codeinextensions(e.g.extensions: { code: "FORBIDDEN" }) rather than relying on clients to string-match the human-readablemessage.
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 genericupdateEntity(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.
[](https://markdowners.com/m/markdowners/graphql-conventions)Discussions about this module
No discussions about this module yet.
Start a discussion
Comments (0)
Sign in to comment. Sign in
No comments yet. Be the first to add one.