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.