Skip to Content
Correlation IDForwarding to the Next Service

Unreleased changes

These pages document main, which has changes 2.1.1 does not. npm install @evanion/nestjs-correlation-id gives you 2.1.1, documented here. Values on these pages are produced by running the current source on every build.

Forward the id to the next service

To send the correlation id on every call Baize’s orders service makes to stock, pass withCorrelation() to HttpModule.registerAsync().

Register HttpModule with withCorrelation

withCorrelation() from @evanion/nestjs-correlation-id returns options for @nestjs/axios, whose HttpModule needs axios beside it. Install both:

npm install @nestjs/axios axios
yarn add @nestjs/axios axios
pnpm add @nestjs/axios axios

StockClient is the orders service’s own client for stock. It is an ordinary HttpService consumer, and nothing in it names a correlation id:

import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; @Injectable() export class StockClient { constructor(private readonly http: HttpService) {} async level(game: string): Promise<{ game: string; available: number }> { const url = `${process.env['STOCK_URL']}/stock/${encodeURIComponent(game)}`; const response = await firstValueFrom(this.http.get(url)); return response.data; } }

The STOCK_URL environment variable holds the base URL of your stock service. StockModule gives StockClient an HttpService whose calls carry the id of the correlation context they are made in, the context CorrelationIdMiddleware opened for the request:

import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { withCorrelation } from '@evanion/nestjs-correlation-id'; import { StockClient } from './stock.client.js'; @Module({ imports: [HttpModule.registerAsync(withCorrelation({ timeout: 5000 }))], providers: [StockClient], exports: [StockClient], }) export class StockModule {}

withCorrelation() takes ordinary HttpModuleOptions and hands them to axios unchanged, so timeout: 5000 applies to every call StockClient makes. Import StockModule into the module whose providers inject StockClient.

Why withCorrelation reads the id on every call

withCorrelation() from @evanion/nestjs-correlation-id registers an axios request interceptor on the instance HttpModule creates. The interceptor calls getCorrelationId() as each request goes out, so one HttpService, built once at boot, sends each request’s own id.

The shorter-looking alternative is a headers entry in HttpModuleOptions. Nest builds those options once, at boot, outside any request, so a headers entry that reads getCorrelationId() there gets undefined, and every call goes out without an id. To rebuild the options per request, Nest needs a request-scoped provider, and it makes every provider that injects a request-scoped one request-scoped too. HttpService, StockClient and every provider holding them would then be built again for each request. Nest calls no lifecycle hook such as onModuleInit on a request-scoped provider, so the onModuleInit of a provider in that chain would never run.

See the header arrive at the stock service

The code below runs both sides of an @evanion/nestjs-correlation-id hop in one test, so it needs no second service of yours. It has three parts:

  • A plain Node HTTP server stands in for stock and answers with the X-Correlation-Id header it received.
  • NestFactory.createApplicationContext() builds the orders service with no HTTP server of its own. Its module imports CorrelationModule.forRoot() and HttpModule.registerAsync(withCorrelation()), as AppModule and StockModule do.
  • The orders service asks stock for the game urn:game:azul, a Baize catalogue id that encodeURIComponent escapes for the URL path. It asks once inside a correlation context, the scope CorrelationService.run() opens (Overview), and once outside one.
/// <reference types="node" /> import { } from 'node:events'; import { } from 'node:http'; import type { AddressInfo } from 'node:net'; import { , } from '@nestjs/axios'; import { } from '@nestjs/core'; import { } from 'rxjs'; import { , , , } from '@evanion/nestjs-correlation-id'; // Baize's stock service, answering with the correlation id it received. const = ((, ) => { .(.['x-correlation-id'] ?? 'no id'); }); // Port 0 asks the OS for a free port, and address() reports which one. await (.(0), 'listening'); const { } = .() as AddressInfo; const = `http://127.0.0.1:${}/stock/${('urn:game:azul')}`; // The orders service, as a Nest application context with no HTTP server. Its // module is a dynamic module: a plain object with `module` and `imports`, the // shape forRoot() returns, so the test declares no decorated class. const = await .( { : class {}, : [ .(), .(()), ], }, { : false }, ); const = .(); const = .(); const = await .('storefront-4f1c9a', () => (.()), ); const = await (.()); await .(); .(); .; // -> 'storefront-4f1c9a' .; // -> 'no id'

Inside run('storefront-4f1c9a', …) the call carried the id. Outside any context the interceptor attached no header, so a call from a cron job that never opened a context carries none.

Check the hop between your own services

To see @evanion/nestjs-correlation-id carry an id between two running services, you write two handlers this page does not show:

  • In stock, a handler for GET /stock/:game that logs getCorrelationId(). stock needs the setup from Getting started for the id to be there.
  • In orders, a handler that calls StockClient.level(), in a module that imports StockModule.

Send X-Correlation-Id: storefront-4f1c9a to the orders handler’s route with curl -H, as Getting started did for OrdersController. The line stock logs for that call carries storefront-4f1c9a.

Where forwarding the correlation id stops

withCorrelation() from @evanion/nestjs-correlation-id has these limits:

  • withCorrelation() needs CorrelationModule.forRoot() in the application. Without it, Nest fails at boot with Nest can't resolve dependencies of the @evanion/nestjs-correlation-id:AXIOS_INTERCEPTOR (AXIOS_INSTANCE_TOKEN, ?, @evanion/nestjs-correlation-id:CORRELATION_CONFIG).
  • The interceptor sends the id under the header that the header field of forRoot() names, X-Correlation-Id by default, so the next service has to read that header.
  • withCorrelation() returns options for HttpModule.registerAsync(). HttpModule.register() takes no extra providers, so it cannot carry the interceptor.
  • withCorrelation() takes the options object and writes the useFactory itself, so it takes no inject list and cannot inject ConfigService. Read an environment value from process.env where the module is declared.
  • Only calls through an HttpService registered with withCorrelation() carry the id, as the Overview lists under what the package leaves to you.

Carry the id where no request arrives

StockClient now sends stock the id @evanion/nestjs-correlation-id holds for the request orders is serving. A queue consumer serves no request, so the middleware opens no context for it. Carry the id through a job opens one by hand and reuses the id the message carried.

Last updated on