Singleton, Per-Request, or Per-Use
Injection Scopes
By default Nest providers are singletons. Sometimes you want a fresh instance per request — or even per injection site.
What you'll learn
- Recognize the three scopes Nest supports
- Pick a scope deliberately and know the trade-offs
- Understand why scopes bubble up the dependency graph
Every provider in Nest has a scope that controls how often it’s instantiated. The default — singleton — is right 95% of the time, but knowing the alternatives matters for per-request state.
The Three Scopes
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.DEFAULT }) // singleton (default)
export class CacheService {}
@Injectable({ scope: Scope.REQUEST }) // one per HTTP request
export class RequestContext {}
@Injectable({ scope: Scope.TRANSIENT }) // new for every injection site
export class RandomId {} - DEFAULT — one instance for the whole app, cached forever. Cheap and fast.
- REQUEST — Nest builds a new instance for each incoming request and throws it away when the response is sent.
- TRANSIENT — every class that injects it gets its own copy. Useful for stateful helpers that shouldn’t be shared.
When to Use Request Scope
The classic case is per-request context: the current user, a trace ID, a tenant. You can read the request directly:
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
constructor(@Inject(REQUEST) private readonly req: Request) {}
get userId() {
return this.req.user?.id;
}
} Inject RequestContext anywhere in the request lifecycle and you’ll get the
right userId without threading it through method args.
Scopes Bubble Up
This is the gotcha. If a singleton depends on a request-scoped provider, Nest can’t keep it as a singleton — the singleton would capture a stale request. So the parent becomes request-scoped too.
@Injectable() // looks singleton...
export class UsersService {
constructor(private readonly ctx: RequestContext) {}
// ^^^^ request-scoped
}
// ...but Nest will treat UsersService as request-scoped at runtime. Cascading like this can quietly turn most of your graph into request-scoped
objects, slowing things down. The fix is usually ClsService (from
nestjs-cls) or AsyncLocalStorage — both give per-request state without
the rebuild cost.
Transient in Practice
Transient is rare but handy when a provider stores per-consumer state, like a logger that prefixes messages with the consumer’s class name:
@Injectable({ scope: Scope.TRANSIENT })
export class ScopedLogger {
setContext(ctx: string) { this.context = ctx; }
private context = '';
log(msg: string) { console.log(`[${this.context}] ${msg}`); }
}