Request-Scoped Providers and Their Trade-offs
Use REQUEST scope safely while understanding performance and injection-bubbling implications.
Why Scopes Exist
By default every NestJS provider is a singleton: one instance is created at bootstrap and shared across the whole application lifetime. This is fast and memory-efficient, and it is the right choice for the overwhelming majority of services.
But some scenarios need per-request state. In a multi-tenant backend you might want a provider that already knows which tenant the current request belongs to, so you do not pass the tenant id through every method call. NestJS solves this with injection scopes.
Scope.DEFAULT— singleton (the default)Scope.REQUEST— a new instance per incoming requestScope.TRANSIENT— a new instance for each consumer that injects it
Declaring a Request-Scoped Provider
You opt into request scope by passing { scope: Scope.REQUEST } to the @Injectable() decorator. NestJS will then instantiate a fresh copy of this provider for every HTTP request (or message, in microservices/WebSocket transports).
Because a new instance exists per request, it is safe to store request-specific mutable state on it — concurrent requests never share the same object.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
private tenantId: string | null = null;
set(id: string): void {
this.tenantId = id;
}
get(): string {
if (!this.tenantId) {
throw new Error('Tenant not resolved for this request');
}
return this.tenantId;
}
}All lessons in this course
- Tenant Resolution via Middleware and AsyncLocalStorage
- Schema-per-Tenant Database Connections
- Building Configurable Dynamic Modules
- Request-Scoped Providers and Their Trade-offs