GraphQL SDL is compact enough to hide a serious contract: a misplaced ! changes how failures propagate to clients. Treat schema punctuation as product behaviour, not resolver scaffolding.
type Query { order(id: ID!): Order }
type Order { id: ID!, totalCents: Int!, notes: String }| Type | Contract |
|---|---|
String | Value can be null |
String! | Resolver must supply a value |
[User] | List and entries can be null |
[User!]! | List exists and every entry exists |
Non-null failures travel upward
If totalCents resolves null, GraphQL nulls Order until it reaches a nullable parent. Make a field non-null only when the service can genuinely uphold that promise across old data and partial outages.
A
JSONscalar is not a shortcut around schema design. It removes validation, discoverability, and typed client generation for data that probably has a stable shape.
Evolve the contract deliberately
Add fields, deprecate old ones with a reason, observe client usage, then remove on a published timeline. For expected business failures, model a union such as CreateOrderResult = Order | ValidationError; do not force clients to infer intent from a generic server error. Cursor connections deserve an explicit type too—large lists should not quietly become unbounded response bodies.
The schema is the public API. Resolvers are its implementation detail.
