Injection Scopes

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.

4 min read Level 3/5 #nestjs#di#scopes
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}`); }
}
Custom Providers →