Injection Scopes
Give a provider one instance per request with the REQUEST token, one per consumer with TRANSIENT and INQUIRER, and see what bubbling does to the classes in between.
What you'll learn
- Declare DEFAULT, REQUEST and TRANSIENT scope, and predict how many instances the container creates
- Inject the current request into a request-scoped provider with the REQUEST token, and explain why scope bubbles up
- Inject INQUIRER into a transient provider to learn which class owns it
Everything injected so far has been a singleton: one instance, created at boot, shared by every request for the life of the application. That is the right default for almost every provider, because a request handler that shares nothing with other requests is fast and needs no locking. But some things are about one request: who sent it, its trace id, the tenant it belongs to. And some things should never be shared at all, because each consumer wants its own. Nest expresses this as a provider's scope, and the container creates instances accordingly.
Three scopes
DEFAULT: one instance for the application, created at bootstrap. Everything you have written so far.REQUEST: a new instance for each incoming request, thrown away when the request has been answered.TRANSIENT: a new instance for each consumer that injects it. Two classes injecting the same transient provider get two instances; a request never shares one.
The scope goes in @Injectable(), or in a custom provider's object:
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {}
{ provide: 'CACHE', useClass: MemoryCache, scope: Scope.TRANSIENT }
A controller takes a scope too, through the options form of its decorator: @Controller({ path: 'cats', scope: Scope.REQUEST }).
Scope bubbles up
A request-scoped provider cannot be held by a singleton: the singleton lives forever, the provider lives for one request, so which request's instance would it hold? Nest resolves this the only way it can. Anything that injects a request-scoped provider becomes request-scoped itself, all the way up the chain, controller included. The controller is then re-created for every request, and so is every provider between it and the request-scoped one. Nothing needs marking; the scope propagates. Transient scope does not bubble: a singleton may inject a transient provider and keeps the instance it was given.
The cost is real. Creating a provider tree per request instead of once at boot adds around 5% latency in Nest's own measurements, and it grows with the size of the tree that bubbles. Keep request-scoped providers small and low in the graph, and keep the services with the logic singletons that receive request data as arguments.
The request itself
A request-scoped provider usually exists to look at the request. The REQUEST token from @nestjs/core injects it:
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
readonly tenant: string;
constructor(@Inject(REQUEST) request: Request) {
this.tenant = String(request.headers['x-tenant'] ?? 'public');
}
}
Every handler down the chain can now inject TenantContext and never touch the request. In GraphQL applications the token is CONTEXT instead.
Who asked
A transient provider can learn which class it was created for through the INQUIRER token, which resolves to the instance that injected it:
@Injectable({ scope: Scope.TRANSIENT })
export class PrefixedLogger {
constructor(@Inject(INQUIRER) private readonly owner: object) {}
log(message: string) {
console.log(`[${this.owner.constructor.name}] ${message}`);
}
}
That is how a logger prints the name of the service it belongs to without being told, and it only works because each consumer has its own instance.
Durable providers
A request-scoped tree is rebuilt for every request. For multi-tenant applications where the tree depends on the tenant and not on the request, Nest can build it once per tenant and reuse it: a durable provider, @Injectable({ scope: Scope.REQUEST, durable: true }), together with a ContextIdStrategy registered in main.ts that maps requests to tenants. It is the way to have per-tenant instances at singleton cost. Know it exists; reach for it when a profiler says so.
Your task
The cats API wants to know which request it is answering, and to tag lines with their owner.
RequestContextshould be built once per request, from that request: itsidis thex-request-idheader, oranonymouswithout one.Taggershould be a fresh instance for every class that injects it, andowner()should name that class.
GET /cats/whoami reports the instances that answered: the context's, the service's, and the tagger's owner. GET /stats/whoami asks a second tagger who it belongs to. Every class counts its instances in a module-level variable, so the numbers show what the container did.
When it fails
contextInstanceis 1 on every request andrequestIdnever changes: the provider is still a singleton, built once at boot.Nest can't resolve dependencies of the RequestContext (?): the constructor asks for the request without theREQUESTtoken.Requestis a type, not a provider.serviceInstancegrows:CatsServicewas given a scope. It should stay a singleton; the controller becoming request-scoped does not change the singletons it injects.tagOwneris the same in both controllers: the tagger is not transient, so one instance serves both andINQUIRERwas the first to ask.
Remember
- Singleton by default;
REQUESTfor one per request,TRANSIENTfor one per consumer. - Request scope bubbles up to everything that injects it, controller included; transient scope does not.
@Inject(REQUEST)gives a request-scoped provider the request;@Inject(INQUIRER)tells a transient provider who owns it.- Scopes cost instances per request; keep the logic in singletons and pass request data down.
Stuck? Show a hint
RequestContext: @Injectable({ scope: Scope.REQUEST }) and a constructor parameter decorated with @Inject(REQUEST), typed Request, whose headers['x-request-id'] is the id when it is a string. Tagger: @Injectable({ scope: Scope.TRANSIENT }) and a constructor parameter decorated with @Inject(INQUIRER); owner() returns its constructor's name.