Skip to content
intermediate Phase 4 · Resolvers and Data Source Integration

The Context Object

Passing request-scoped data like auth, data sources, and loaders through context.

40m
1 problems
Topic Progress 0%

One Object, Built Once per Request

One Object, Built Once per Request

A nested query might call forty resolvers. Each one needs the authenticated user, a database handle, and the request id for logs — and none of them can take those as explicit parameters, because the executor calls resolvers with exactly four arguments and you do not own the call site.

So the server builds one object per request and hands it to every resolver as the third argument:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ request }) => {
    const user = await authenticate(request.headers.authorization);
    return {
      currentUser: user,                    // who is making this request
      dataSources: makeDataSources(dbPool), // repositories for this request
      requestId: request.headers['x-request-id'] || randomUUID(),
      loaders: makeLoaders(dbPool),         // caches must not outlive the request
    };
  },
});
// any resolver, at any depth
deletePost: async (_p, { id }, ctx) => {
  const post = await ctx.dataSources.posts.byId(id);
  if (!post || post.authorId !== ctx.currentUser.id) throw new ForbiddenError();
  await ctx.dataSources.posts.remove(id);
  return { deletedId: id };
}

The mental model: context is request-scoped dependency injection. It is constructed before execution starts, shared by every resolver in that execution, and dropped with the request. Resolvers never build their own collaborators, so replacing the database with an in-memory fake in tests means swapping one factory instead of stubbing forty call sites.

Two things it is not. It is not a cross-request cache — that is a shared object you create deliberately outside the handler. And it is not a home for single-field values; those arrive from the enclosing resolver as parent, as covered in resolver fundamentals.

What Goes In, What Stays Out

What Goes In, What Stays Out

Context earns its place only when the boundary is deliberate.

Belongs in context:

Entry Why
currentUser authentication ran once; every resolver can assume identity
dataSources one pool and one set of repositories for the whole request
loaders per-request batching caches — see request scoped loaders
requestId / trace id a single identifier tying every resolver's logs together
logger a child logger with the request id already attached
pubsub handle the event bus subscriptions publish to

Does not belong:

  • Field-level values. post, user, author arrive as parent. Copying them into context creates two sources of truth for the same data.
  • Module-scope mutable state. Declaring const context = { currentUser: null } at startup turns context into a process-wide global — the third chapter shows what that costs.
  • Business policy. Context carries capability: this is the user, this is the pool. Deciding whether that user may archive a specific order belongs in the resolver or a directive.

That last split keeps authentication and authorization in their natural homes. The context factory answers who is calling — token parsing, session lookup, a failure that rejects the request before execution — while resolvers and directives answer may this caller do this for one field. Moving the first out of resolver bodies is what makes role based field access and authorization directives enforceable in one place instead of scattered checks.

One rule to state out loud in code review: treat context as read-only after creation. A resolver that writes ctx.currentUser = someoneElse edits an object the rest of the operation is still reading, and every field resolved after that line silently inherits the change. Derive a local variable instead.

The upstream mechanics of resolving identity from a request are in request auth context.

The Shared Context Leak

The Shared Context Leak

The most expensive context bug looks like a small optimization:

// module scope — ONE object for the whole process
const context = { currentUser: null, dataSources: null };

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ request }) => {
    context.currentUser = await authenticate(request.headers.authorization);
    context.dataSources = makeDataSources(pool);
    return context;   // same object every time
  },
});

Single-user testing passes. Under concurrency it leaks identities:

t0  request A: context.currentUser = alice
t1  request B: context.currentUser = bob      // overwrites alice
t2  request A: resolver reads ctx.currentUser -> bob

Alice's request executes with Bob's identity: another user's data in the response, and an authorization bypass that reproduces only when two requests overlap. It shows up as "flaky cross-account reports" and never reproduces with one curl session.

The fix is a fresh object inside the handler:

context: async ({ request }) => ({
  currentUser: await authenticate(request.headers.authorization),
  dataSources: makeDataSources(pool),
  requestId: request.headers['x-request-id'] || randomUUID(),
}),

Three checks that keep it fixed:

  1. No context object literal at module scope. If one exists outside the handler, every request shares it.
  2. Resolvers never assign to ctx. With a shared object, one branch writing ctx.currentUser poisons the rest of the request — and, in the buggy version, other requests as well.
  3. Loaders are constructed in the factory. A module-scope dataloader batches keys across different users' requests, which is both a leak and a correctness hazard (request scoped loaders).

When a deep library needs request state you do not want to thread through ten signatures, Node's AsyncLocalStorage carries it along the async call chain instead of living on a shared object.

Interview angle: "how does the authenticated user reach a resolver?" Answer the per-request factory, then immediately name the failure mode of building it too early. Interviewers who keep pushing on concurrency are checking whether you have run the server under parallel load, not just in GraphiQL.

Practice Problems

0 / 1 solved
Fix the Cross-Request Context Leak
Request Scoping

A GraphQL server intermittently returns another user's data under load. The server builds context at module scope and mutates it per request. Identify the race, show the timeline that produces a wrong-user response, and rewrite the server setup so identity, loaders, and data sources are request-scoped.

Example:

Input: Two requests overlap: alice's query starts resolving while bob's token is being verified

Output: Buggy version: alice's resolvers read currentUser = bob. Fixed version: each request keeps its own object, so alice's resolvers always read alice.

The buggy code returns the same object to every request, so whichever request writes last determines what every in-flight resolver sees.

Optimal Solution — O(1) time, O(1) space

Create a fresh context object inside the per-request handler and keep every request-scoped dependency inside it.

// BEFORE (buggy)
const context = { currentUser: null, dataSources: null };
server.context = async ({ request }) => {
  context.currentUser = await authenticate(request.headers.authorization);
  context.dataSources = makeDataSources(pool);
  return context;                       // shared by every concurrent request
};

// AFTER (fixed)
server.context = async ({ request }) => {
  const currentUser = await authenticate(request.headers.authorization); // throws -> 401 before execution
  return {
    currentUser,
    dataSources: makeDataSources(pool),
    loaders: makeLoaders(pool),         // new caches, never reused across requests
    requestId: request.headers['x-request-id'] || randomUUID(),
  };
};

Edge Cases:

  • Missing or invalid token must fail in the factory, not halfway through a mutation
  • Serverless instances that reuse the process across requests still need per-request objects
  • A loader hoisted "for performance" reintroduces the same leak with cached rows

Quiz

1. When is the context object created relative to resolver execution?

Question 1 options

2. Which value belongs in context?

Question 2 options

3. What symptom reveals a context object created at module scope?

Question 3 options

4. Where are authentication and authorization separated in this design?

Question 4 options

Flashcards

Question

When context is built

Answer

Once per request by the context factory, before execution; every resolver in that request receives the same object.

Question

Context vs parent vs info

Answer

context = request-scoped deps (user, data sources, loaders); parent = value the enclosing field resolved; info = AST, path, schema metadata.

Question

Module-scope context bug

Answer

A shared context object mutated per request leaks identities under concurrency — the last writer wins. Always create a fresh object inside the handler.

Question

Authentication vs authorization placement

Answer

Factory authenticates once (who); resolvers/directives authorize per field (may they do this).

Question

Why loaders must be per request

Answer

Their caches key on request data; sharing one across requests batches different users together and serves stale rows.

Revision Notes

Key Takeaways

  • 1. Context is request-scoped dependency injection: built once, shared by all resolvers, discarded with the request
  • 2. It carries capability (user, pool, loaders, request id) — not resolved field values or business policy
  • 3. Creating context at module scope leaks identity between concurrent requests; create it inside the handler
  • 4. Resolvers treat context as read-only, and loaders are constructed fresh per request

Interview Tips

  • • Answer "how do you pass the current user to resolvers" with the factory, then volunteer the shared-object leak as the failure mode you design against
  • • Be ready to say what you would never store in context and why — parent values, mutable counters, and process config are the three interviewers probe for

Cheat Sheet

Resolver Context Cheat Sheet

context: async ({ request }) => ({
  currentUser: await authenticate(request.headers.authorization),
  dataSources: makeDataSources(pool),
  loaders: makeLoaders(pool),      // fresh caches per request
  requestId: request.headers['x-request-id'] || randomUUID(),
})
  • Created once per request, before execution; read-only from resolvers
  • In: currentUser, dataSources, loaders, requestId/logger, pubsub
  • Out: parent values, per-field results, module-scope mutable state, business policy
  • Factory authenticates (who); resolvers/directives authorize (may they)
  • Bug signature: const context = {...} at module scope → cross-request identity leak
  • Deep ambient state without threading params: AsyncLocalStorage

Continue Learning