Photo by PJH on Unsplash

On this page

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:

resolver-signature.ts
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):

context.ts
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:

author-loader.ts
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.

Next: Building a GraphQL server with NestJS.

Check your understanding

Question 1 of 7

0 of 7 checked

  1. 1. Which argument carries per-request services such as database clients?

Search articles

Type a word to search published articles.

Open the search page