advanced
Resolvers, N+1, and DataLoader
Keep resolvers thin, prevent N+1 data access, batch and cache per request, and preserve authorization boundaries.
Resolvers map schema fields to data. Naive per-field DB calls cause **N+1**: one query for N parents plus N child queries.
// N+1: loads customer per order
const resolvers = {
Query: {
orders: () => db.order.findMany(),
},
Order: {
customer: (order) => db.customer.findById(order.customerId), // called N times
},
};
**DataLoader** batches and caches loads per request:
const customerLoader = new DataLoader(async (ids) => {
const customers = await db.customer.findByIds(ids);
const map = new Map(customers.map((c) => [c.id, c]));
return ids.map((id) => map.get(id) ?? null);
});
const resolvers = {
Order: {
customer: (order, _args, ctx) => ctx.loaders.customer.load(order.customerId),
},
};
Resolver guidelines:
- Keep resolvers thin; business logic in services.
- Create loaders per request context (no cross-request cache leaks).
- Authorize in parent resolver or field middleware — do not fetch data before auth checks when possible.
- Use query-level DataLoader or JOIN/strategy for list+relation patterns.
On interviews: explain N+1 detection (logs, tracing), batching semantics, and why DataLoader cache is request-scoped.
Common pitfalls: global DataLoader singleton, authorization only in UI, returning internal ORM entities with lazy relations, and missing batch for polymorphic relations.
The trade-off is resolver simplicity versus explicit data fetching — consider DataLoader + service layer over fat ORM magic.
Checklist:
- One loader instance per request.
- Batch by key; preserve result order.
- Auth before expensive fetches where feasible.
- Trace resolver depth and query count.