This chapter follows Building a GraphQL server with NestJS.
A schema that mirrors database tables one-to-one often forces clients to stitch fields that product design already treats as one screen. Query-driven design starts from UI or API consumer needs, then maps fields to services and storage.
Start from client operations
Collect the GraphQL documents your first clients need (or write them in design review). If a screen loads upcomingEvents { name location { weather { temperature } } }, the schema should expose that graph directly instead of forcing five REST-shaped queries.
Keep internal microservice boundaries inside resolvers; the public schema stays stable while backends split or merge.
Naming conventions
| Element | Convention |
|---|---|
| Fields and arguments | camelCase |
| Types and inputs | PascalCase |
| Enum values | ALL_CAPS |
Mutations should be verbs (createPost, archiveOrder). Queries are nouns or noun phrases (post, searchPosts). Consistency helps codegen and logs.
Nullability strategy
Prefer non-null for fields clients always need when the parent exists. Use nullable fields for optional enrichment (weather on Location) so one failed dependency does not bubble away the whole parent if you mark the parent nullable carefully.
Adding ! to an formerly optional output field is breaking for clients that must handle null. Removing ! changes error bubbling—also risky.
Cursor connections for pagination
Offset pagination (limit/offset) is simple but performs poorly on large tables. Relay cursor connections standardize pagination:
type Query { posts(first: Int, after: String): PostConnection!}
type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo!}
type PostEdge { node: Post! cursor: String!}
type PageInfo { hasNextPage: Boolean! endCursor: String}Clients pass after: endCursor from the previous page. NestJS and other libraries offer @nestjs/graphql plugins or helpers to generate connection types.
Mutation input and payload pattern
Group write arguments in a single input and return a payload with the changed entity:
input UpdateEmailInput { userId: ID! email: String!}
type UpdateEmailPayload { user: User errors: [UserError!]}
type Mutation { updateEmail(input: UpdateEmailInput!): UpdateEmailPayload!}Returning the updated user avoids a second round trip. Listing errors in the payload suits expected validation failures.
Errors as data versus the errors array
GraphQL’s top-level errors array fits unexpected failures and partial null bubbling. Domain errors (email taken, insufficient stock) are often clearer as union or interface results:
union CreateOrderResult = Order | ValidationError
type ValidationError { message: String! field: String}Clients branch with __typename without treating business rules as transport exceptions. Reserve errors for truly exceptional resolver throws and log them server-side.
Next: GraphQL clients compared.
Check your understanding
Question 1 of 6
0 of 6 checked