typescript 80 lines · 4 tabs

Propagate a Request ID Through NestJS Logs with AsyncLocalStorage

Shared by codesnips Jul 2026
4 tabs
import { AsyncLocalStorage } from 'node:async_hooks';

interface Store {
  requestId: string;
}

const storage = new AsyncLocalStorage<Store>();

export const RequestContext = {
  run<T>(requestId: string, callback: () => T): T {
    return storage.run({ requestId }, callback);
  },

  getRequestId(): string | undefined {
    return storage.getStore()?.requestId;
  },
};
4 files · typescript Explain with highlit

This snippet shows how to attach a stable request ID (a correlation ID) to every log line emitted while handling a single HTTP request in NestJS, without threading the ID manually through every service call. The mechanism is Node's AsyncLocalStorage, which carries a per-request store across every await and callback in the async chain, so any code running under a given request can read the same context.

In RequestContext, an AsyncLocalStorage<Store> instance holds a small Store object with the requestId. The run helper seeds the store for the duration of a callback, and getRequestId reads the current value, returning undefined outside any request scope. Wrapping the store in a dedicated module keeps the storage a singleton and hides the AsyncLocalStorage API behind two intention-revealing functions.

In RequestIdMiddleware, an incoming request either reuses an inbound x-request-id header (useful for tracing a call across services) or mints a fresh randomUUID. The ID is echoed back on the response header so clients and downstream systems can correlate, then RequestContext.run is invoked around next(). Because middleware sits at the very start of the request lifecycle, everything that executes after next() — controllers, providers, guards, even async work — runs inside the same store.

In ContextLogger, a custom LoggerService reads RequestContext.getRequestId() at log time and prefixes each message with the ID. Reading the ID lazily inside format rather than injecting it means a single logger instance works for all requests; there is no need for a request-scoped provider, which avoids the per-request instantiation cost that Scope.REQUEST imposes on the whole injection subtree.

In main.ts, the middleware is registered globally and the custom logger is wired via app.useLogger, so framework logs and application logs share the same enrichment. The main trade-off is that AsyncLocalStorage has a small overhead and can lose context if code escapes the async chain (for example, unmanaged timers or certain native callbacks). For request-scoped correlation, though, it is far cheaper and simpler than request-scoped DI, and it keeps service signatures clean.


Related snips

Share this code

Here's the card — post it anywhere.

Propagate a Request ID Through NestJS Logs with AsyncLocalStorage — share card
Link copied