NestJS dependency injection container
- Last updated
- Prerequisites:
- TypeScript decorators basics
- NestJS modules and providers
- nestjs
- dependency-injection
- container
- typescript
- reflect-metadata
- decorators
Read at your depth
The practical view
In NestJS you mark a service with @Injectable(), list it in a module's providers, and inject it via a constructor parameter: constructor(private readonly usersService: UsersService) {}. Nest resolves the graph at bootstrap: it instantiates the provider (and all its transitive dependencies), caches it per the provider scope (DEFAULT = singleton per application, REQUEST = per request, TRANSIENT = per injection), and injects it where requested. Injection happens via the TypeScript design:paramtypes metadata that the compiler emits when decorators are present (reflect-metadata + emitDecoratorMetadata).
Legacy vs modern
Manual service wiring vs Nest DI container
Hand-constructed services tangle creation order and make swaps invasive; the container resolves the graph, applies scope, and enables clean test substitution.
const repo = new UserRepository(db);
const svc = new UserService(repo);
const ctrl = new UserController(svc);@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {}
}Interview gotchas
Context
This is the core 'explain the magic' question — the interviewer wants the reflect-metadata + design:paramtypes mechanism, not just 'decorators'.
The mechanical answer
TypeScript emits metadata when decorators and emitDecoratorMetadata are enabled: for a class with a decorator, the compiler writes the constructor parameter types into a design:paramtypes metadata key on the class. Nest's runtime reads that metadata with reflect-metadata (Reflect.getMetadata) to discover the injected tokens. Because the metadata contains the type references only for class-based types, string/abstract/symbol tokens need @Inject(Token) to be discoverable — there is no design-time type for them. The @Injectable() decorator itself is the marker that makes Nest scan this class and read its metadata.
Trap
The naive answer is 'TypeScript types are available at runtime'. They are not — the metadata is only emitted for decorated classes and only for parameter types that are actual runtime references (classes). Candidates who say 'Nest uses the types from the constructor signature' miss that emitDecoratorMetadata is what makes it possible and that interface-based injection fails without @Inject. Mentioning the exact metadata key (design:paramtypes) is the differentiator.
Context
This checks practical Nest experience — the interviewer wants to know how scope propagates and why request-scoped providers are expensive.
The mechanical answer
DEFAULT providers are singletons per application: instantiated once, cached in the module's instance map. REQUEST providers are instantiated per request and garbage-collected after; TRANSIENT providers get a new instance per injection site. Scope propagates: if a singleton depends on a request-scoped provider, Nest must make the singleton effectively request-scoped too (it cannot be cached across requests), which the framework does by elevating the whole dependency chain. That means one request-scoped provider can turn an entire controller chain into per-request instantiation — measurable cost in allocation and constructor work for high-traffic services. The guideline is to keep request scope only where request context is genuinely needed (tenant isolation, per-request state) and avoid it in hot paths.
Trap
The trap answer is 'request scope is free in Express because Nest is just middleware'. Request-scoped providers bypass the singleton cache and are created per request, and their dependencies must be recreated too. Another trap: 'transient is like request' — transient is per injection site, not per request. Candidates who describe scope propagation up the dependency chain and the per-request allocation cost show real production awareness.