Request-Scoped Providers
By default NestJS providers are singletons: one instance is shared across every request for the lifetime of the application. That is fast and memory-efficient, but sometimes you need a fresh instance per request — to capture the authenticated user, a correlation ID, or a tenant identifier without threading that state through every method call. Request-scoped providers solve this by instantiating the provider once per inbound request, and they let you inject the underlying REQUEST object so your services can read per-request context directly.
When you actually need request scope
Most of the time you do not. A singleton service that receives the data it needs as method arguments is simpler and dramatically faster. Reach for Scope.REQUEST only when a provider must hold state tied to a single request and that state is awkward to pass explicitly — for example, a logger that prefixes every line with a request ID, or a repository that must scope queries to the current tenant resolved from a JWT.
| Scope | Instances | Lifetime | Use when |
|---|---|---|---|
Scope.DEFAULT | One (singleton) | App lifetime | Almost always |
Scope.REQUEST | One per request | Single request | Need per-request state/context |
Scope.TRANSIENT | One per consumer | Per injection | Each consumer needs a private instance |
Declaring a request-scoped provider
Set the scope option on @Injectable(). NestJS then creates a new instance of this provider for every incoming request and disposes of it when the request completes.
// request-context.service.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
readonly correlationId: string;
readonly tenantId: string | undefined;
constructor(@Inject(REQUEST) private readonly request: Request) {
this.correlationId =
(request.headers['x-correlation-id'] as string) ?? crypto.randomUUID();
this.tenantId = request.headers['x-tenant-id'] as string | undefined;
}
describe(): string {
return `tenant=${this.tenantId ?? 'none'} corr=${this.correlationId}`;
}
}
The REQUEST token comes from @nestjs/core. With the Express adapter it resolves to the Express Request; under Fastify it resolves to the Fastify FastifyRequest. Type it accordingly.
Injecting and consuming the provider
Any provider that injects a request-scoped provider automatically becomes request-scoped too — scope “bubbles up” the dependency chain. The same is true for the controller that consumes it.
// orders.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { RequestContextService } from './request-context.service';
@Injectable({ scope: Scope.REQUEST })
export class OrdersService {
constructor(private readonly ctx: RequestContextService) {}
list(): { context: string; items: string[] } {
return { context: this.ctx.describe(), items: ['order-1', 'order-2'] };
}
}
// orders.controller.ts
import { Controller, Get } from '@nestjs/common';
import { OrdersService } from './orders.service';
@Controller('orders')
export class OrdersController {
constructor(private readonly orders: OrdersService) {}
@Get()
findAll() {
return this.orders.list();
}
}
Output:
$ curl -H "x-tenant-id: acme" -H "x-correlation-id: 8f2a" http://localhost:3000/orders
{"context":"tenant=acme corr=8f2a","items":["order-1","order-2"]}
Scope bubbles upward, never downward. Injecting a request-scoped provider into a singleton makes the singleton request-scoped as well. Be deliberate about where you introduce it, or you may accidentally convert large parts of your graph to per-request instantiation.
Performance implications
Request-scoped providers are not cached. On every request Nest walks the relevant part of the injection container and constructs fresh instances, which adds latency and garbage-collection pressure under load. A controller that is request-scoped is re-instantiated for each call, so any expensive constructor work runs per request.
Practical guidance:
- Keep the scoped portion of your graph small — isolate per-request state in one thin provider and inject it rather than scoping the whole service tree.
- Never put request scope on a provider that opens connections or does heavy initialization in its constructor.
- Singletons can still read request data via durable providers (below) or by accepting it as arguments.
Accessing REQUEST without making the consumer scoped
If a singleton needs request data only occasionally, inject the REQUEST token directly into a narrowly-scoped helper, or use ContextIdFactory / ModuleRef.resolve() to look up a scoped instance on demand instead of converting the consumer.
import { Injectable } from '@nestjs/common';
import { ModuleRef, ContextIdFactory } from '@nestjs/core';
@Injectable()
export class ReportService {
constructor(private readonly moduleRef: ModuleRef) {}
async build() {
const contextId = ContextIdFactory.create();
const ctx = await this.moduleRef.resolve(RequestContextService, contextId);
return ctx.describe();
}
}
Durable providers for multi-tenancy
In multi-tenant systems creating a brand-new instance per request is wasteful when many requests share the same tenant. Durable providers let Nest key scoped instances by a custom value — typically the tenant ID — so instances are reused across requests that resolve to the same key.
Implement a ContextIdStrategy that derives a sub-tree-payload from the request, then mark the provider durable: true.
// tenant.strategy.ts
import {
HostComponentInfo,
ContextId,
ContextIdFactory,
ContextIdStrategy,
} from '@nestjs/core';
import { Request } from 'express';
const tenants = new Map<string, ContextId>();
export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = (request.headers['x-tenant-id'] as string) ?? 'public';
let tenantSubTreeId = tenants.get(tenantId);
if (!tenantSubTreeId) {
tenantSubTreeId = ContextIdFactory.create();
tenants.set(tenantId, tenantSubTreeId);
}
return (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId! : contextId;
}
}
// main.ts
import { ContextIdFactory } from '@nestjs/core';
import { AggregateByTenantContextIdStrategy } from './tenant.strategy';
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantConnection {
/* one instance shared per tenant, not per request */
}
Now TenantConnection is created once per tenant and reused, giving you the isolation of request scope with most of the efficiency of a singleton.
Best Practices
- Default to singletons; introduce
Scope.REQUESTonly when per-request state is genuinely required. - Keep scoped providers tiny and free of expensive constructor logic to limit per-request cost.
- Type the injected
REQUESTobject to your platform (express.Requestor Fastify’sFastifyRequest). - Remember that scope bubbles upward — auditing one new scoped provider may reveal an unexpectedly large request-scoped subtree.
- Use durable providers to amortize instantiation in multi-tenant apps where many requests share a key.
- Prefer
ModuleRef.resolve()with aContextIdover converting a singleton when access is occasional.