NestJS in Production · Configuration, Testing, Data and Operations
Request Context Without Request-Scoped Providers
Request-scoped providers recreate whole dependency chains per request; AsyncLocalStorage carries a request ID and user cheaply.
What you'll build
Two ways to make the current request ID available deep inside services: a Scope.REQUEST provider (and its cost: every dependent provider is recreated per request) and an AsyncLocalStorage-based RequestContext set by middleware.
The idea
- A request-scoped provider is created for every request, and so is everything that depends on it, up to the controller. In a large app that means many object creations per request.
AsyncLocalStorage(Node's async context) stores per-request data that follows async calls, so singletons can read it without changing scope.- Set the context once in middleware: request ID, user, tenant; read it in loggers and repositories.
The code
import "reflect-metadata";
import { Controller, Get, Inject, Injectable, MiddlewareConsumer, Module, NestModule, Scope } from "@nestjs/common";
import { NestFactory, REQUEST } from "@nestjs/core";
import { AsyncLocalStorage } from "node:async_hooks";
let created = 0;
@Injectable({ scope: Scope.REQUEST })
class RequestIdHolder { constructor(@Inject(REQUEST) req: any) { created++; this.id = req.headers["x-request-id"]; } id: string; }
@Injectable()
class ScopedRepo { constructor(private holder: RequestIdHolder) { created++; } find() { return `query tagged ${this.holder.id}`; } }
const als = new AsyncLocalStorage<{ requestId: string }>();
@Injectable()
class AlsRepo { find() { return `query tagged ${als.getStore()?.requestId}`; } }
@Controller("scoped")
class ScopedController { // depends on a request-scoped provider, so it becomes request-scoped too
constructor(private repo: ScopedRepo) { created++; }
@Get() find() { return this.repo.find(); }
}
@Controller("als")
class AlsController { // plain singleton chain
constructor(private repo: AlsRepo) { created++; }
@Get() find() { return this.repo.find(); }
}
@Module({ controllers: [ScopedController, AlsController], providers: [RequestIdHolder, ScopedRepo, AlsRepo] })
class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply((req: any, res: any, next: () => void) => als.run({ requestId: req.headers["x-request-id"] }, next)).forRoutes("*path");
}
}
async function main() {
const app = await NestFactory.create(AppModule, { logger: false });
await app.listen(0, "127.0.0.1");
const base = `http://127.0.0.1:${app.getHttpServer().address().port}`;
for (const [path, id] of [["/scoped", "r-1"], ["/scoped", "r-2"], ["/als", "r-3"], ["/als", "r-4"]]) {
const before = created;
const text = await (await fetch(base + path, { headers: { "x-request-id": id } })).text();
console.log(`${path}: ${text} (objects created for this request: ${created - before})`);
}
await app.close();
}
main();
Output
/scoped: query tagged r-1 (objects created for this request: 3) /scoped: query tagged r-2 (objects created for this request: 3) /als: query tagged r-3 (objects created for this request: 0) /als: query tagged r-4 (objects created for this request: 0)
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Making a widely used provider (logger, repository) request-scoped, silently turning large parts of the app request-scoped.
- Passing request IDs through every function signature by hand.
- Forgetting that work started outside a request (queues, cron jobs) has no context; set one for each job.
Key points
- Request scope bubbles up the dependency chain and costs per request.
- AsyncLocalStorage carries per-request data for singletons.
- Set the context in middleware (or job handlers) and read it where needed.
Further reading: Injection scopes · Async local storage · Node.js AsyncLocalStorage
Practice questions
What does 'scope bubbles up' mean in NestJS?
Answer
A provider that depends on a request-scoped provider becomes request-scoped itself, all the way up to the controller, so whole chains are created per request.
How does AsyncLocalStorage help in NestJS?
Answer
It stores per-request data that follows the async call chain, so singleton services can read the request ID or user without becoming request-scoped.
Where should the request context be set?
Answer
In middleware at the start of each request (and in job handlers for background work), wrapping the rest of the processing in als.run().
Open in the app to mark lessons complete, take notes and track your progress.