This chapter follows Writing GraphQL operations.
Validation succeeds; execution begins at the root operation fields and walks the selection set depth-first in field order. Each scalar leaf passes through a resolver function (or a default that reads a property). How you structure resolvers determines latency, authorization, and database load.
The resolver signature
Every field resolver receives four arguments:
type ResolverFn = ( parent: unknown, args: Record<string, unknown>, context: GraphQLContext, info: GraphQLResolveInfo,) => unknown;| Argument | Typical use |
|---|---|
parent |
Value returned by the parent field’s resolver |
args |
Field arguments from the client |
context |
Per-request services: auth, DB, DataLoaders |
info |
Query AST, return type, path (for logging or projections) |
Root Query resolvers often ignore parent. Nested resolvers use parent as the DTO or ORM entity for the object type.
Default resolvers
If you omit a resolver, many servers use a default that returns (parent as any)[fieldName] when parent is an object. That works for simple mappings but hides N+1 fetches when parent is only an ID stub.
Explicit resolvers clarify side effects and authorization checks.
Execution order and concurrency
Query fields at the same level may run in parallel. Mutation top-level fields run serially. Nested fields under a parent generally run after the parent resolves; sibling fields may parallelize depending on the engine.
Resolvers should be pure with respect to global mutable state; store connections and user identity on context, not module-level variables.
Per-request context
Create context once per HTTP request (or WebSocket connection for subscriptions):
export interface GraphQLContext { userId: string | null; loaders: { author: DataLoader<string, Author> }; db: DbClient;}Pass tokens from headers in a context factory. NestJS uses context: ({ req }) => ({ ... }) on GraphQLModule; Apollo Server and Yoga expose similar hooks.
The same pattern works outside Nest: the idea is identical even if the bootstrap API differs.
The N+1 problem
A list query that resolves a child field per row can trigger one database query per item:
query { books { title author { name } }}If books returns 50 rows and each author resolver queries SQL, you pay 51 queries. The fix is batching: collect keys during a tick of execution and fetch in one round trip.
DataLoader
DataLoader batches loads and caches within a single request:
import DataLoader from "dataloader";
export function createAuthorLoader(db: DbClient) { return new DataLoader<string, Author>(async (ids) => { const rows = await db.authors.findByIds([...ids]); const map = new Map(rows.map((r) => [r.id, r])); return ids.map((id) => map.get(id) ?? new Error(`Author ${id}`)); });}Attach loaders to context so each request gets a fresh cache. In NestJS, provide loaders request-scoped in a dedicated provider.
Errors and null bubbling
When a resolver throws, the field becomes null if its return type is nullable. If the type is non-null (!), the error bubbles to the parent; the parent may also become null and add another error entry.
Design nullable boundaries at API edges where partial data is acceptable (for example optional author on Book). Use try/catch in resolvers to map domain failures to user-safe errors without leaking stack traces—especially in production.
Check your understanding
Question 1 of 7
0 of 7 checked