Forward the id to the next service
@evanion/nestjs-correlation-id 2.1.0 and its patch releases up to 2.1.1, the version on npm. Values on this page were produced by running the source at 77650ee, a later commit than the 2.1.0 tag, and are not re-executed.
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 axiosyarn add @nestjs/axios axiospnpm add @nestjs/axios axiosStockClient 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
stockand answers with theX-Correlation-Idheader it received. NestFactory.createApplicationContext()builds theordersservice with no HTTP server of its own. Its module importsCorrelationModule.forRoot()andHttpModule.registerAsync(withCorrelation()), asAppModuleandStockModuledo.- The
ordersservice asksstockfor the gameurn:game:azul, a Baize catalogue id thatencodeURIComponentescapes for the URL path. It asks once inside a correlation context, the scopeCorrelationService.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 forGET /stock/:gamethat logsgetCorrelationId().stockneeds the setup from Getting started for the id to be there. - In
orders, a handler that callsStockClient.level(), in a module that importsStockModule.
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()needsCorrelationModule.forRoot()in the application. Without it, Nest fails at boot withNest 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
headerfield offorRoot()names,X-Correlation-Idby default, so the next service has to read that header. withCorrelation()returns options forHttpModule.registerAsync().HttpModule.register()takes no extra providers, so it cannot carry the interceptor.withCorrelation()takes the options object and writes theuseFactoryitself, so it takes noinjectlist and cannot injectConfigService. Read an environment value fromprocess.envwhere the module is declared.- Only calls through an
HttpServiceregistered withwithCorrelation()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.