Skip to Content
Correlation IDGetting Started

Ahead of the release

npm install @evanion/nestjs-correlation-id gives you 2.0.0. These pages document main, which has changes that release does not.

Getting started

Register the module and the middleware

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { CorrelationIdMiddleware, CorrelationModule, } from '@evanion/nestjs-correlation-id'; @Module({ imports: [CorrelationModule.forRoot()], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(CorrelationIdMiddleware).forRoutes('*'); } }

forRoot() provides CorrelationService and the resolved configuration; the middleware opens a correlation context per request.

forRoot() registers a global module, so importing it once in the root module is enough. Call it once — a second forRoot() registers a second configuration provider under the same token, and the last import wins.

Register the middleware early. Everything downstream of it — guards, interceptors, controllers, and anything they await — runs inside the context; anything applied before it does not.

What the middleware does per request

  1. Reads the configured header, case-insensitively. Node lowercases incoming header names, so only the response casing depends on how you spelled it.
  2. If a value arrived and passes validate, that value is the correlation id. Otherwise it calls the generator.
  3. Writes the id onto req.headers when the request carried none, so anything reading the header rather than the service — a proxy, an access logger, a framework-level request logger — sees the same id. A header that arrived is left exactly as it came in, including one validate rejected.
  4. Sets the response header, unless something has already set it.
  5. Runs the rest of the request inside CorrelationService.run(id, next).

The generator runs only when nothing usable arrived, so a counter- or sequence-backed generator is not advanced for an id that gets discarded.

Reading the id

CorrelationService is a singleton. Inject it like any other provider:

import { CorrelationService } from '@evanion/nestjs-correlation-id'; import { Injectable } from '@nestjs/common'; @Injectable() export class OrdersService { constructor(private readonly correlation: CorrelationService) {} async place(order: Order) { this.logger.log({ correlationId: this.correlation.getCorrelationId() }); } }

getCorrelationId() is synchronous and returns undefined when there is no correlation context. Outside one there genuinely is no correlation id, so it does not invent one.

Nothing about injecting the service changes the scope of the provider holding it.

Adding it to logs

import { CorrelationService } from '@evanion/nestjs-correlation-id'; import { Injectable, NestMiddleware } from '@nestjs/common'; import type { IncomingMessage, ServerResponse } from 'node:http'; import * as Sentry from '@sentry/node'; @Injectable() export class SentryTagMiddleware implements NestMiddleware { constructor(private readonly correlation: CorrelationService) {} use(_req: IncomingMessage, _res: ServerResponse, next: () => void) { const correlationId = this.correlation.getCorrelationId(); if (correlationId) Sentry.setTag('correlationId', correlationId); next(); } }
configure(consumer: MiddlewareConsumer) { consumer.apply(CorrelationIdMiddleware).forRoutes('*'); consumer.apply(SentryTagMiddleware).forRoutes('*'); }

Order matters: CorrelationIdMiddleware is what opens the context, so anything reading the id has to be applied after it.

Forwarding on outgoing calls

import { HttpModule } from '@nestjs/axios'; import { withCorrelation } from '@evanion/nestjs-correlation-id'; @Module({ imports: [HttpModule.registerAsync(withCorrelation())], controllers: [UsersController], providers: [UsersService], }) export class UsersModule {}

Use HttpService as usual. It stays a singleton: the header is attached by an axios request interceptor that reads the current context at the moment the request is made, rather than being baked into the options object when the factory runs.

One id across two processes. The middleware reuses the header the caller sent or mints one, and runs the rest of the request inside that context; the interceptor withCorrelation() installs reads the same context when HttpService calls the next service, which reuses the id in turn.

That keeps the whole chain singleton-scoped. Options computed at factory time would have to come from a request-scoped provider, and HttpService — and every provider holding it — would become request-scoped with it.

withCorrelation() takes ordinary HttpModuleOptions:

HttpModule.registerAsync(withCorrelation({ timeout: 5000 }));

It needs CorrelationModule.forRoot() somewhere in the application. Without it, Nest fails at boot:

Nest can't resolve dependencies of the HTTP_MODULE_OPTIONS (?)

When there is no correlation context, no header is attached. An outgoing call from a cron job that never opened one carries none.

Work with no request behind it

Queue consumers, cron jobs, scripts — anything with no incoming request — opens a context itself:

await this.correlation.run(this.correlation.generate(), () => this.processJob(job), );

run returns whatever the callback returns, so an async callback’s promise comes straight back out. Everything the callback awaits, schedules or calls sees the id, and overlapping contexts stay isolated.

To carry an id that arrived on the job payload rather than minting one:

await this.correlation.run( job.correlationId ?? this.correlation.generate(), () => this.processJob(job), );

Validate a payload-supplied id the same way the middleware validates a header-supplied one — see the validator. An id from a queue reaches the same log lines a header-supplied one does.

Replacing the id

this.correlation.setCorrelationId('some_correlation_id');

It throws outside a correlation context, rather than writing somewhere nothing will read:

setCorrelationId() was called outside a correlation context. Apply CorrelationIdMiddleware, or wrap the work in CorrelationService.run().

setCorrelationId changes the id the rest of the request sees. The response header keeps the id the middleware wrote when the request arrived.

Why @evanion/nestjs-correlation-id has nothing to click

Every other section on this site puts a control under the reader’s hand on its demonstration page. This one does not, and the reason is the package rather than the budget: the demonstrable unit is two running services and one header between them, and nothing inside a documentation page can be the second process. A simulated caller would prove something about the simulation.

apps/shop-api in this repository is the real pair. It applies CorrelationIdMiddleware to every route, and the shop’s storefront reads the id back off the response, so the runnable version of this page is the shop with both processes up.

Last updated on