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;
},
};
import { Injectable, NestMiddleware } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { Request, Response, NextFunction } from 'express';
import { RequestContext } from './request-context';
const HEADER = 'x-request-id';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
const inbound = req.headers[HEADER];
const requestId =
(Array.isArray(inbound) ? inbound[0] : inbound) || randomUUID();
res.setHeader(HEADER, requestId);
RequestContext.run(requestId, () => next());
}
}
import { ConsoleLogger, Injectable, LoggerService } from '@nestjs/common';
import { RequestContext } from './request-context';
@Injectable()
export class ContextLogger extends ConsoleLogger implements LoggerService {
private format(message: unknown): string {
const requestId = RequestContext.getRequestId() ?? '-';
return `[req:${requestId}] ${String(message)}`;
}
log(message: unknown, context?: string): void {
super.log(this.format(message), context);
}
error(message: unknown, stack?: string, context?: string): void {
super.error(this.format(message), stack, context);
}
warn(message: unknown, context?: string): void {
super.warn(this.format(message), context);
}
debug(message: unknown, context?: string): void {
super.debug(this.format(message), context);
}
}
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RequestIdMiddleware } from './request-id.middleware';
import { ContextLogger } from './context.logger';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
const logger = app.get(ContextLogger);
app.useLogger(logger);
app.use(new RequestIdMiddleware().use);
await app.listen(3000);
logger.log('Application listening on port 3000', 'Bootstrap');
}
bootstrap();
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
# Grafana provisioning: datasources
# /etc/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
Grafana dashboards as code with JSON provisioning
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.