iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…