GraphQL Schema Design

The schema is the contract of a GraphQL API. It declares every type, field, argument, query, and mutation a client can use, and the server guarantees responses match it. Because clients select exactly the fields they need and tooling generates types from the schema, a well-designed schema is a pleasure to use for years. A poorly designed one leaks database tables, forces awkward client code, and becomes hard to change without breaking someone.

GraphQL APIs are usually evolved continuously rather than versioned, so design decisions like nullability, mutation shapes, and error modeling matter up front. This page covers the principles that make schemas intuitive, safe, and evolvable.

TL;DR

Quick Example

Clients handle each outcome with type-safe branches instead of parsing error strings.

Core Concepts

Schema-First vs Code-First

Both are fine. What matters is that the generated or written schema is reviewed as the API contract, ideally with schema diffs in pull requests.

Design From Use Cases

A schema mirroring your database (user_id foreign keys, join tables as types) forces clients to understand your storage. Instead:

Nullability

In GraphQL, if a non-null field fails to resolve, the null propagates up to the nearest nullable parent, potentially wiping out a large part of the response. Guidelines:

Mutations: Input and Payload

A consistent mutation shape makes APIs predictable and evolvable:

Interfaces, Unions, and Global IDs

Errors as Data

GraphQL's top-level errors array is untyped and easy for clients to ignore. For expected business errors (validation failures, not found, permission denied for a specific action, conflicts), return them in the schema, either as result unions like the example above, or as a userErrors: [UserError!]! field on payloads. Reserve top-level errors for unexpected failures such as bugs and outages.

Evolving a Schema

GraphQL APIs typically avoid /v2 versions. Because clients request fields explicitly, you can add new fields and types without affecting anyone.

Use a schema registry (Apollo GraphOS, Hive, Inigo) or schema-diff checks in CI to detect breaking changes, and field-usage analytics to confirm a deprecated field is unused before removing it. See API versioning.

Best Practices

Use Custom Scalars for Meaningful Types

DateTime, URL, Email, Decimal, and Money types communicate intent and validate input, unlike stringly-typed fields.

Paginate Every Unbounded List

Any list that can grow (orders, comments, items) needs pagination arguments from day one. Retrofitting pagination is a breaking change. See GraphQL pagination.

Document Everything in the Schema

Descriptions ("""…""") appear in GraphiQL, IDEs, and generated docs. Document units, formats, nullability semantics, and side effects.

Keep Naming Consistent

Use camelCase fields, PascalCase types, and SCREAMING_SNAKE_CASE enum values; name inputs XInput and payloads XPayload or XResult; and give booleans a prefix: isActive, hasShipped, canEdit.

Common Mistakes

Exposing Database Structure

Generic "Update Everything" Mutations

updateUser(input: UpdateUserInput!) with 30 optional fields hides business rules, makes authorization per field complicated, and is hard to evolve. Prefer task-oriented mutations like changeEmail and updateShippingAddress.

Non-Null Everywhere

Marking every field ! looks tidy, but one failing resolver can null out an entire query result, and relaxing non-null later is a breaking change. Be deliberate.

FAQ

Should GraphQL schemas be versioned?

Generally no. The common practice is continuous evolution: add fields, deprecate old ones, monitor usage, and remove only when unused. Some public APIs publish dated schema versions for stability guarantees, but URL-versioned GraphQL APIs are rare.

How should I return validation errors?

As data in the schema: either a union of success and error types, or a userErrors list on the mutation payload with field paths and messages. This makes error handling explicit and type-checked in clients, rather than hidden in the generic top-level errors array.

Schema-first or code-first?

Schema-first suits teams that design APIs collaboratively and want the SDL as the reviewed source of truth. Code-first suits teams that want compile-time guarantees that resolvers match the schema, with no duplication. Many mature teams use code-first but still review the generated SDL diff in PRs.

How big should a GraphQL schema be?

As big as the domain requires. Large companies run schemas with thousands of types, often composed from many services with GraphQL federation. Size matters less than consistency of naming, patterns, and ownership.

Related Topics

References