Schema design that scales with your org
Your schema is an API contract and an org chart. Design it for the consumers you want, not the databases you have.
Model the domain, not the tables
The single most common mistake we see in graph reviews: schemas that mirror the storage layer.
# ❌ Storage leaking into the API
type OrderRow {
order_id: ID!
cust_fk: ID!
status_code: Int!
}
# ✅ Domain modelling
type Order {
id: ID!
customer: Customer!
status: OrderStatus!
}
enum OrderStatus { PENDING PAID SHIPPED CANCELLED }
Consumers should never need to know your join keys or status integers. Enums over magic numbers, object references over foreign keys, always.
Nullability is a promise, not a default
Every non-null field (!) is a promise that this field can never fail independently. In a distributed graph, a non-null field whose resolver fails nulls out its whole parent chain — one flaky downstream service can blank an entire page.
Rules of thumb:
- IDs and enums on the type itself: non-null.
- Anything resolved from another service or datasource: nullable, even if it "should always exist".
- Lists: prefer
[Thing!](nullable list, non-null items) so a failure returnsnull, not a list with holes.
Design for the client's screen, not for reuse
A field that tries to serve every consumer serves none well. It's fine — good, even — to have Product.cardSummary and Product.detailView style fields shaped for specific surfaces. Generic "one true type" schemas push formatting logic into every client and change constantly.
Naming conventions we enforce
| Rule | Example |
|---|---|
| Queries are nouns | order(id:), not getOrder |
| Mutations are verb-first | cancelOrder, addLineItem |
| Mutations return a payload type | CancelOrderPayload with the changed entity + user errors |
| Booleans read as assertions | isCancellable, not cancellable |
| No abbreviations in public fields | quantity, not qty |
Mutations: payload types with user errors
Don't throw GraphQL errors for business outcomes. Reserve top-level errors for system failures; model expected failures in the schema:
type CancelOrderPayload {
order: Order
userErrors: [UserError!]!
}
type UserError {
field: [String!]
message: String!
code: ErrorCode!
}
Clients can then build UI against typed, predictable failure modes — and your error monitoring stays clean for real incidents.
Review checklist
- No storage identifiers, join keys or status integers in public types
- Cross-service fields are nullable
- Every mutation returns a payload type with
userErrors - Enums for every closed set of values
- Deprecations carry a
reasonwith a migration path (see Versioning & deprecation)