How NestJS Dependency Injection Works with Providers: Complete Guide to the DI Container
NestJS implements a sophisticated dependency injection container that automatically resolves provider dependencies by scanning constructor parameters, looking up tokens in module registries, and injecting cached or fresh instances based on defined scopes.
NestJS dependency injection serves as the architectural backbone of the framework, enabling loose coupling and testable components through a powerful IoC (Inversion of Control) container. When you decorate a class with @Injectable(), the framework registers it as a provider within a module's context, allowing automatic resolution and injection into controllers, services, and other providers. The internal mechanics of this system—as implemented in the nestjs/nest repository—reveal how the NestInjector class manages singleton, request-scoped, and transient lifecycles through a deterministic resolution algorithm.
Core Concepts of the NestJS DI System
Understanding the dependency injection architecture requires familiarity with four fundamental components that work together in the core package.
Providers and the @Injectable() Decorator
A provider is any class annotated with @Injectable() or defined as a custom provider token that the container can instantiate. According to the source code in packages/core/decorators/injectable.decorator.ts, this decorator registers metadata about the class and optionally accepts a scope configuration. Providers can also be non-class values using useValue, useFactory, or useClass syntax, allowing strings, symbols, or class references to serve as injection tokens.
Modules as DI Boundaries
Modules in NestJS act as logical units that encapsulate related providers, controllers, and components. As defined in packages/core/module/module.ts, each module maintains an internal provider map populated from the providers array in @Module() metadata. The exports array determines which providers are visible to other modules that import this module, creating a hierarchical resolution chain.
The NestInjector Class
The core resolution logic resides in packages/core/injector/injector.ts, where the NestInjector class stores a map of tokens to instances and performs the resolution algorithm. This injector maintains separate registries for singleton, request-scoped, and transient providers, ensuring that instance creation follows the declared lifetime strategy.
Provider Scopes
The scope determines how long a provider instance lives. The Scope enum in packages/core/constants/scope.enum.ts defines three distinct lifetimes:
- Singleton (
DEFAULT): One instance per application lifecycle (default behavior) - Request (
REQUEST): New instance per incoming HTTP request - Transient (
TRANSIENT): New instance every time the provider is injected
The Provider Resolution Flow
When you bootstrap an application with NestFactory.create(AppModule), the DI container executes a five-phase resolution process.
Module Registration and Metadata Scanning
During bootstrap, Nest scans all @Module() decorators recursively. Each provider listed in the providers array is registered in the module's internal provider map. The framework uses the reflect-metadata API to examine the constructor signatures of each class, extracting parameter types that become injection tokens.
Dependency Graph Construction
For every provider, Nest builds a dependency graph by analyzing constructor parameters. Each parameter type becomes a token that must resolve to another provider. The system detects circular dependencies during this phase and throws appropriate errors before instantiation begins.
Token Resolution Strategy
When a class requires a dependency, the injector performs a hierarchical lookup:
- Check the current module's provider map for the requested token
- If not found, traverse the imported modules hierarchy recursively
- Return the first matching provider or throw an
Unknown providerexception
This resolution strategy ensures that providers remain encapsulated within modules unless explicitly exported.
Instance Creation and Caching
The instantiation behavior varies by scope:
- Singleton: The injector creates the instance once during bootstrap and caches it in
packages/core/injector/injector.ts. All subsequent injections receive the identical reference. - Request-scoped: The injector generates a context ID for each incoming request and stores a per-request instance map. Controllers and providers marked with
Scope.REQUESTreceive unique instances for that specific HTTP request lifecycle. - Transient: The injector bypasses caching entirely, invoking
newon the provider class every time it appears in a constructor, even within the same request.
Provider Scopes in Practice
Different scopes solve different architectural problems. Here are concrete implementations of each scope type.
Singleton Scope (Default)
The default scope maintains a single instance throughout the application lifetime, ideal for stateless services like database connections or utility classes.
// src/common/services/users.service.ts
@Injectable()
export class UsersService {
findAll() {
return ['Alice', 'Bob'];
}
}
// src/app.controller.ts
@Controller()
export class AppController {
constructor(private readonly usersService: UsersService) {}
@Get('users')
getUsers() {
return this.usersService.findAll();
}
}
Request-Scoped Providers
Use Scope.REQUEST when you need request-specific state, such as tracking request IDs or tenant contexts. As implemented in packages/core/injector/injector.ts, the container creates a new instance for every incoming HTTP request.
// src/common/providers/request-id.provider.ts
@Injectable({ scope: Scope.REQUEST })
export class RequestIdService {
private readonly id = Math.random().toString(36).substring(2, 15);
getId() {
return this.id;
}
}
// src/app.controller.ts
@Controller()
export class AppController {
constructor(private readonly requestId: RequestIdService) {}
@Get('request-id')
getId() {
return { requestId: this.requestId.getId() };
}
}
Transient Providers
Transient scope creates a fresh instance for every injection site, useful for random number generators or isolated calculation engines where you must guarantee separate internal states.
// src/common/providers/random-number.provider.ts
@Injectable({ scope: Scope.TRANSIENT })
export class RandomNumberService {
private readonly value = Math.random();
getValue() {
return this.value;
}
}
// src/app.controller.ts
@Controller()
export class AppController {
constructor(
private readonly rnd1: RandomNumberService,
private readonly rnd2: RandomNumberService,
) {}
@Get('random')
getRandom() {
// Two different numbers because the provider is transient
return { first: this.rnd1.getValue(), second: this.rnd2.getValue() };
}
}
Custom Provider Tokens
Beyond class-based injection, NestJS supports custom provider tokens that inject non-class values using string or symbol identifiers. This pattern appears frequently for configuration objects or third-party libraries.
// src/common/constants.ts
export const CONFIG_TOKEN = 'CONFIG_TOKEN';
// src/common/config.provider.ts
{
provide: CONFIG_TOKEN,
useValue: { apiUrl: 'https://api.example.com' },
}
// src/app.service.ts
@Injectable()
export class AppService {
constructor(@Inject(CONFIG_TOKEN) private readonly config: { apiUrl: string }) {}
getApiUrl() {
return this.config.apiUrl;
}
}
The @Inject() decorator from packages/core/decorators/inject.decorator.ts tells the injector to look up the CONFIG_TOKEN string in the provider map rather than using TypeScript's type metadata.
Summary
- NestJS dependency injection relies on the
NestInjectorclass inpackages/core/injector/injector.tsto maintain a registry of tokens and instances across three scopes: singleton, request, and transient. - Modules encapsulate providers and control visibility through the
providersandexportsarrays, while the injector walks the import hierarchy to resolve missing tokens. - Provider scopes determine instance caching strategy: singleton instances persist for the application lifecycle, request-scoped instances last for a single HTTP request, and transient instances are created fresh for every injection site.
- Custom tokens enable injection of primitive values and factory results using
@Inject()and custom provider syntax (useValue,useFactory,useClass).
Frequently Asked Questions
What is the difference between singleton and transient scope in NestJS?
Singleton scope (the default) creates one instance per application that is reused across all injections, while transient scope creates a new instance every time the provider appears in a constructor. According to the source code in packages/core/injector/injector.ts, singleton instances are cached in the injector's internal map, whereas transient providers bypass caching entirely. Use singleton for stateless services like database connections, and transient when you need guaranteed isolation between injection sites.
How does NestJS resolve dependencies across module boundaries?
The injector implements a hierarchical resolution algorithm defined in packages/core/injector/injector.ts. When a token is not found in the current module's provider map, the injector recursively checks the imports array of the module, looking for exported providers from other modules. This creates a resolution chain where providers are only accessible if their host module explicitly lists them in the exports array and the consuming module imports that host module.
Can I inject non-class values using NestJS dependency injection?
Yes, NestJS supports custom provider tokens that allow injection of strings, objects, or factory results using the @Inject() decorator. You define these providers using object syntax with provide (the token), and useValue, useFactory, or useClass to specify the implementation. The injector stores these values in the same token map as class providers, making them available for constructor injection alongside standard @Injectable() services.
Where is the dependency injection container implemented in the NestJS source code?
The core container logic resides in packages/core/injector/injector.ts, which contains the NestInjector class responsible for provider registration, token resolution, and instance lifecycle management. Supporting files include packages/core/decorators/injectable.decorator.ts (the @Injectable() decorator), packages/core/module/module.ts (module encapsulation logic), and packages/core/constants/scope.enum.ts (scope definitions). For testing utilities, packages/core/testing/testing-module.builder.ts demonstrates how to programmatically construct DI containers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →