Context
GraphQL context: per-request data (auth, datasources, request id) injected into every resolver. The clean way to share state.
GraphQL — resolver context
EXAMPLE
// ===== What context is =====
// A per-request object that resolvers receive as their THIRD argument.
// Use it for: authenticated user, db handles, dataloaders, logger, request id.
// ===== Apollo Server =====
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.replace(/^Bearer /, '');
const user = token ? await verifyJwt(token) : null;
return {
user,
db,
loaders: createDataLoaders(),
requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
};
},
});
// In resolvers:
const resolvers = {
Query: {
me: (_parent, _args, ctx) => {
if (!ctx.user) throw new GraphQLError('Unauthorized', { extensions: { code: 'UNAUTHORIZED' } });
return ctx.db.users.findById(ctx.user.id);
},
},
};
// ===== GraphQL Yoga =====
import { createYoga } from 'graphql-yoga';
const yoga = createYoga({
schema,
context: ({ request }) => ({
user: getUserFromAuth(request.headers.get('authorization')),
db,
loaders: createDataLoaders(),
}),
});
// ===== Patterns =====
// 1. Per-request DataLoaders to batch DB lookups:
import DataLoader from 'dataloader';
function createDataLoaders() {
return {
userById: new DataLoader(async (ids) => {
const rows = await db.users.findManyByIds(ids);
return ids.map(id => rows.find(r => r.id === id));
}),
};
}
// In resolver:
User: {
author: (post, _, ctx) => ctx.loaders.userById.load(post.author_id),
};
// Batches all .load() calls in the same tick into ONE query.
// 2. Auth helpers on the context:
function makeContext({ req }) {
const user = getUserFromReq(req);
return {
user,
requireUser() {
if (!user) throw new GraphQLError('Unauthorized', { extensions: { code: 'UNAUTHORIZED' } });
return user;
},
requireRole(role) {
const u = this.requireUser();
if (!u.roles.includes(role)) throw new GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' } });
return u;
},
};
}
// 3. Logger with requestId:
import pino from 'pino';
const baseLogger = pino();
function makeContext({ req }) {
const requestId = req.headers['x-request-id'] ?? crypto.randomUUID();
return {
logger: baseLogger.child({ requestId, path: req.url }),
};
}
// ===== Subscriptions =====
// Long-lived; context built once per subscription, not per message.
// Be careful with mutable refs.
// ===== Patterns to internalise =====
// - DataLoader per request (NEVER global; would cache across users)
// - Auth verified once in context; resolvers only check role / scope
// - Pass a child logger with requestId for traceability
// - Keep context small + typed; do not stuff random utilities
// ===== Pitfalls =====
// - Global DataLoaders -> cache poisoning across users
// - Mutating context during resolvers -> subtle bugs
// - Synchronous errors thrown in context function -> request fails awkwardly
// - Long-running async work in context() -> increases TTFB for every request
Why it matters
Context is the per-request DI container: user, db, loaders, logger, requestId. Create DataLoaders per request, expose auth helpers, and inject a child logger with the request id. Keep it small + typed; do not stuff utility methods that belong elsewhere.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Pass per-request goodies — user, db, dataloader — via context.
createYoga({ schema, context: ({ request }) => ({ user: getUser(request) }) });
Try it Yourself »
Discussion
Loading…