Skip to content

SaifuddinTipu/express-correlation-context

Repository files navigation

express-correlation-context

npm version npm downloads CI license tests

One middleware. Access request context — correlation ID, IP, method, path, duration, custom fields — from anywhere in your call stack without passing req around.

Uses Node.js AsyncLocalStorage under the hood. Zero dependencies. Works with Express 4 and 5.


The problem

// Without this library — you pass req through every layer
router.get('/orders', async (req, res) => {
  const orders = await orderService.list(req);        // need req for logging
  await emailService.notify(req, orders);             // need req for correlation ID
  await auditService.log(req, 'orders.listed');       // need req everywhere
});
// With express-correlation-context — context is available anywhere
router.get('/orders', async (_req, res) => {
  const orders = await orderService.list();           // no req needed
  await emailService.notify(orders);                  // clean signatures
  await auditService.log('orders.listed');            // context flows automatically
});

// Deep inside orderService, emailService, auditService:
import { getContext, getCorrelationId } from 'express-correlation-context';

function logSomething() {
  console.log({ correlationId: getCorrelationId() }); // just works
}

Installation

npm install express-correlation-context

No peer dependencies beyond Express itself.


Quick Start

import express from 'express';
import { correlationContext, getContext, getCorrelationId, setContext } from 'express-correlation-context';

const app = express();

// Register once — before your routes
app.use(correlationContext());

app.get('/hello', (req, res) => {
  const ctx = getContext();
  res.json({
    correlationId: ctx?.correlationId,  // UUID, auto-generated or from header
    duration: ctx?.duration(),          // ms elapsed since request started
    ip: ctx?.ip,
    method: ctx?.method,
    path: ctx?.path,
  });
});

Every request automatically gets:

  • A correlation ID (read from x-correlation-id header or auto-generated UUID)
  • IP, method, path, user-agent captured from the request
  • A duration() function that returns ms elapsed since the request started
  • The correlation ID echoed back in the response header

API

correlationContext(options?)

Express middleware factory. Register it once before your routes.

app.use(correlationContext({
  header: 'x-correlation-id',          // header to read/write. Default: 'x-correlation-id'
  generateId: () => myCustomUUID(),    // custom ID generator. Default: crypto.randomUUID()
  echoHeader: true,                    // echo ID in response header. Default: true
  onContext: (ctx, req) => {           // hook to enrich context after creation
    ctx.userId = (req as any).user?.id;
  },
}));

getContext(): CorrelationContext | null

Returns the full context object for the current request, or null if called outside a request.

const ctx = getContext();
ctx?.correlationId  // string
ctx?.startTime      // Unix ms
ctx?.duration()     // ms elapsed
ctx?.ip             // client IP (respects x-forwarded-for)
ctx?.method         // 'GET', 'POST', etc.
ctx?.path           // '/api/orders'
ctx?.userAgent      // 'Mozilla/5.0...'
ctx?.userId         // any custom field set via setContext()

getCorrelationId(): string | null

Shorthand for getContext()?.correlationId.

import { getCorrelationId } from 'express-correlation-context';

logger.info('Processing job', { correlationId: getCorrelationId() });

setContext(key, value)

Set a custom field on the current request context. Throws if called outside a request.

// In an auth middleware
setContext('userId', decoded.sub);
setContext('tenantId', decoded.tenant);

// Anywhere later in the same request
const ctx = getContext();
console.log(ctx?.userId);    // set earlier
console.log(ctx?.tenantId);  // set earlier

Options Reference

Option Type Default Description
header string 'x-correlation-id' Header name to read and write the correlation ID
generateId () => string crypto.randomUUID() Custom ID generator (e.g. nanoid, cuid)
echoHeader boolean true Whether to echo the ID back in the response header
onContext (ctx, req) => void undefined Hook to enrich context after creation (e.g. extract userId from JWT)

Real-World Patterns

Structured logging (Pino)

import pino from 'pino';
import { getContext } from 'express-correlation-context';

const baseLogger = pino();

// Wrap pino so every log line includes correlationId automatically
export const logger = {
  info: (msg: string, data?: object) =>
    baseLogger.info({ correlationId: getContext()?.correlationId, ...data }, msg),
  error: (msg: string, data?: object) =>
    baseLogger.error({ correlationId: getContext()?.correlationId, ...data }, msg),
};

// Usage — no req needed
logger.info('Order created', { orderId: 'ord_123' });
// → { correlationId: 'a1b2-...', orderId: 'ord_123', msg: 'Order created' }

Extract userId from JWT via onContext

app.use(jwtMiddleware);   // sets req.user
app.use(
  correlationContext({
    onContext: (ctx, req) => {
      const authed = req as Request & { user?: { id: string; role: string } };
      if (authed.user) {
        ctx.userId = authed.user.id;
        ctx.role   = authed.user.role;
      }
    },
  }),
);

// Now available everywhere
const ctx = getContext();
console.log(ctx?.userId);  // 'u_abc123'
console.log(ctx?.role);    // 'admin'

Multi-tenant SaaS — track tenantId

app.use(
  correlationContext({
    onContext: (ctx, req) => {
      ctx.tenantId = req.headers['x-tenant-id'] ?? 'unknown';
    },
  }),
);

// Inside any service
async function createOrder(data: OrderData) {
  const ctx = getContext();
  await db.orders.create({ ...data, tenantId: ctx?.tenantId });
}

Response time header

app.use(correlationContext());

app.use((_req, res, next) => {
  res.on('finish', () => {
    const duration = getContext()?.duration();
    if (duration !== undefined) {
      res.setHeader('x-response-time', `${duration}ms`);
    }
  });
  next();
});

Use with NestJS (global middleware)

// main.ts
import { correlationContext } from 'express-correlation-context';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(correlationContext());
  await app.listen(3000);
}

TypeScript

All exports are fully typed. Extend CorrelationContext for custom fields:

import { getContext, CorrelationContext } from 'express-correlation-context';

interface AppContext extends CorrelationContext {
  userId: string;
  tenantId: string;
}

const ctx = getContext() as AppContext | null;
ctx?.userId;    // typed
ctx?.tenantId;  // typed

How it works

AsyncLocalStorage (available since Node.js 12.17, stable since 16) creates an isolated storage slot that propagates automatically through:

  • async/await
  • Promise.then()
  • setTimeout / setInterval
  • EventEmitter callbacks
  • Any other async continuation in the same request chain

Each incoming request gets its own isolated store. Concurrent requests never see each other's context — guaranteed by the runtime.


License

MIT © Saifuddin Tipu

About

One middleware. Access request context — correlation ID, IP, method, path, duration, custom fields — from anywhere in your call stack without passing req around.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors