Providers & Dependency Injection
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against NestJS 12.1 (npm 12.1.2), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and was checked on 2026-10-02. The examples use a project generated by the Nest 12 CLI (ESM layout, Vitest, supertest); the container is platform-agnostic, so nothing on this page behaves differently on Express and on Fastify (the one adapter-related difference is noted at the end of Providers and the request lifecycle).
A provider is anything the Nest container can create, cache and hand to a class that asks for it: services, repositories, factories, helpers, configuration objects, connections or plain values. Providers are the second building block after modules, and the one that gives Nest its architecture: controllers stay thin and delegate to providers, and providers delegate to other providers. The wiring between them is not done by your code but by the injector, the part of Nest’s IoC (inversion of control) container that reads declared dependencies and builds objects in the right order. This page follows the Bookshelf scenario used across the section: a BooksController that depends on BooksService, which depends on BooksRepository.
@Injectable() and constructor injection
A provider in its simplest form is a class listed in the providers array of a module. The @Injectable() decorator attaches metadata that declares the class as manageable by the container, and the constructor parameter types tell the injector what to supply.
import { Injectable } from '@nestjs/common';
export interface Book {
id: number;
title: string;
}
@Injectable()
export class BooksRepository {
private readonly books: Book[] = [
{ id: 1, title: 'Dune' },
{ id: 2, title: 'Emma' },
];
findAll(): Book[] {
return this.books;
}
}
import { Injectable } from '@nestjs/common';
import { BooksRepository, type Book } from './books.repository.js';
@Injectable()
export class BooksService {
// No @Inject(): the emitted design:paramtypes metadata names BooksRepository.
constructor(private readonly repository: BooksRepository) {}
findAll(): Book[] {
return this.repository.findAll();
}
}
import { Controller, Get } from '@nestjs/common';
import { BooksService } from './books.service.js';
import type { Book } from './books.repository.js';
@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}
@Get()
findAll(): Book[] {
return this.booksService.findAll();
}
}
Registering the classes in a module is what makes them injectable:
import { Module } from '@nestjs/common';
@Module({
controllers: [BooksController],
providers: [BooksRepository, BooksService],
})
export class BooksModule {}
curl http://localhost:3000/books
[{"id":1,"title":"Dune"},{"id":2,"title":"Emma"}]
Source: Providers.
Three details matter here:
-
The
private readonlymodifier makes the parameter a TypeScript parameter property: it declares the member and assigns it, so nothis.repository = repositoryline is needed. -
The type annotation is the injection token. With
emitDecoratorMetadataenabled, TypeScript writes the constructor’s parameter types intodesign:paramtypesfor every class that carries at least one decorator, and the injector reads that array. How that works, and why it fails for interfaces, is covered in TypeScript for NestJS. -
@Injectable()is what makes the compiler emit the metadata for a class without another decorator. Controllers get it from@Controller(). A class with no decorator at all has nodesign:paramtypes, so Nest cannot see its constructor dependencies (see Manual instantiation for a case where this bites).
The CLI generates the class and registers it for you: nest generate service books (alias nest g s books) creates books.service.ts, a spec file, and adds the class to the nearest module’s providers.
How the container resolves a dependency graph
When the application starts, Nest scans the module graph, collects every provider, and builds a dependency graph from the declared constructor parameters. Resolution is transitive and runs bottom-up: a provider is instantiated only after everything it depends on exists. With the default singleton scope, the instance is created once, cached under its token in the module that declares it, and shared with every consumer that can see it.
BooksRepository, BooksService"] R2["CatalogModule.providers
DbOptions, DB_CONNECTION (factory), CatalogService"] end scan --> Q[2. Build the graph from design:paramtypes and @Inject tokens] Q --> A["BooksController needs BooksService"] A --> B["BooksService needs BooksRepository"] B --> C["BooksRepository has no dependencies"] C --> D["3. Instantiate BooksRepository (cached)"] D --> E["4. Instantiate BooksService with the repository"] E --> F["5. Instantiate BooksController with the service"] Q --> G["CatalogService needs DB_CONNECTION"] G --> H["DB_CONNECTION factory needs DbOptions"] H --> I["DbOptions: instantiate first"] I --> J["Run the factory (awaited if async), cache its result"] J --> K["Instantiate CatalogService"] F --> L["onModuleInit hooks, then the app starts listening"] K --> L
Things that follow from this model:
-
Order is derived, not declared. The order of entries in
providersdoes not matter; the graph decides. -
Each module is its own scope for lookups. The injector looks for a token in the declaring module’s own providers, then in the exports of the modules it imports. A provider that exists but is not visible produces the "can’t resolve dependencies" error shown in Diagnosing resolution errors.
-
Failures happen at startup, not on the first request. A missing token, a circular dependency or a rejected async factory stops
NestFactory.create()(orTest.createTestingModule().compile()) before the server listens. -
Providers are instantiated eagerly by default. Only request- and transient-scoped providers are created later, per request or per consumer; those scopes are covered in the injection-scopes material of the Fundamentals chapters.
Source: Custom providers, "DI fundamentals".
Tokens: classes, strings, symbols and abstract classes
Every provider is registered under a token. A class is its own token, which is why providers: [BooksService] works. That form is shorthand for the full provider object:
providers: [
{
provide: BooksService, // the token
useClass: BooksService, // how the value is produced
},
];
A token can also be a string, a Symbol or a TypeScript enum member. Since TypeScript erases interfaces and type aliases, constructor-type resolution works only for something that exists at runtime, so any other token must be named explicitly with @Inject(token).
// A string token, a symbol token and an abstract class used as a token.
export const DB_CONNECTION = 'DB_CONNECTION';
export const CLOCK = Symbol('CLOCK');
export const ISBN_URL = Symbol('ISBN_URL');
export interface Clock {
now(): Date;
}
export abstract class Notifier {
abstract notify(message: string): string;
}
Source: Custom providers, "Non-class-based provider tokens" and Custom providers, "Interfaces and abstract classes".
| Token kind | Use it when | Injected with |
|---|---|---|
Class |
The class is the contract and the implementation (the default). |
The parameter type; no decorator. |
Abstract class |
You want one artifact to be both the TypeScript contract and the runtime token, and to swap implementations with |
The parameter type; no decorator. |
|
You describe a contract with an interface, or you write a library. Each symbol has a unique identity, so unrelated providers cannot collide. |
|
String (or enum member) |
Quick, readable tokens in an application. Keep them in a constants file; two modules that pick the same string silently share a provider. |
|
Whatever the kind, define the token once in a shared file and import it where the provider is registered and where it is injected. For an interface-typed dependency, the correct form is a symbol token plus @Inject():
export interface LoggerService {
log(message: string): void;
}
export const LOGGER_SERVICE = Symbol('LOGGER_SERVICE');
@Module({
providers: [{ provide: LOGGER_SERVICE, useClass: PinoLoggerService }],
})
export class AppModule {}
@Injectable()
export class CatsService {
constructor(@Inject(LOGGER_SERVICE) private readonly logger: LoggerService) {}
}
Custom providers
The shorthand covers the common case. When you need a different value than "one instance of this class", use a provider object with provide and exactly one of useValue, useClass, useFactory or useExisting. All the pieces below are registered by one module:
import { Inject, Injectable, Optional, type Provider } from '@nestjs/common';
import { CLOCK, DB_CONNECTION, ISBN_URL, Notifier, type Clock } from './tokens.js';
// ---- useValue -------------------------------------------------------------
export const fixedClock: Clock = { now: () => new Date('2026-10-02T12:00:00Z') };
export const clockProvider: Provider = { provide: CLOCK, useValue: fixedClock };
// ---- useClass (decided at registration time) -------------------------------
@Injectable()
export class EmailNotifier extends Notifier {
notify(message: string): string {
return `email:${message}`;
}
}
@Injectable()
export class ConsoleNotifier extends Notifier {
notify(message: string): string {
return `console:${message}`;
}
}
export const notifierProvider: Provider = {
provide: Notifier,
useClass: process.env.NOTIFIER === 'email' ? EmailNotifier : ConsoleNotifier,
};
// ---- useFactory with inject (one mandatory, one optional dependency) -------
@Injectable()
export class DbOptions {
get(): { url: string } {
return { url: process.env.DATABASE_URL ?? 'sqlite::memory:' };
}
}
export class DatabaseConnection {
constructor(
readonly url: string,
readonly label: string,
) {}
}
export const connectionProvider: Provider = {
provide: DB_CONNECTION,
useFactory: (options: DbOptions, label?: string) =>
new DatabaseConnection(options.get().url, label ?? 'default'),
inject: [DbOptions, { token: 'CONNECTION_LABEL', optional: true }],
};
// ---- useExisting (alias) ---------------------------------------------------
@Injectable()
export class AuditLogger {
readonly lines: string[] = [];
log(line: string): void {
this.lines.push(line);
}
}
export const loggerAliasProvider: Provider = {
provide: 'AliasedAuditLogger',
useExisting: AuditLogger,
};
// ---- a non-class value produced by a factory -------------------------------
export const isbnUrlProvider: Provider = {
provide: ISBN_URL,
useFactory: () => process.env.ISBN_URL ?? 'https://isbn.example.test/lookup',
};
Source: Custom providers.
The consumer asks for each of them in the way its token requires:
@Injectable()
export class CatalogService {
constructor(
@Inject(DB_CONNECTION) readonly connection: DatabaseConnection,
@Inject(CLOCK) private readonly clock: Clock,
private readonly notifier: Notifier, // abstract class token: no @Inject needed
@Inject('AliasedAuditLogger') readonly audit: AuditLogger,
private readonly directLogger: AuditLogger,
@Inject(ISBN_URL) readonly isbnUrl: string,
@Optional() @Inject('FEATURE_FLAGS') readonly flags?: { beta: boolean },
) {}
describe() {
return {
db: `${this.connection.label}:${this.connection.url}`,
now: this.clock.now().toISOString(),
note: this.notifier.notify('hello'),
flags: this.flags ?? null,
sameLogger: this.audit === this.directLogger,
};
}
}
Running it (with NOTIFIER unset) returned:
{
"db": "default:sqlite::memory:",
"now": "2026-10-02T12:00:00.000Z",
"note": "console:hello",
"flags": null,
"sameLogger": true
}
Each strategy in detail:
useValue-
Provides a value that already exists: a constant, an object from an external library, or a mock. The value is used as is, never instantiated or cached by Nest. Because of structural typing, a mock only needs the same shape as the class it replaces. This is also the standard way to replace a real provider in tests (Overriding providers in tests).
useClass-
Chooses the class that a token resolves to. The chosen class is instantiated and its own constructor dependencies are injected, so
EmailNotifiercan depend on other providers. The conditional expression above runs once, when the module metadata is evaluated, so the decision is taken at import time, not per request. Anything that depends on the configuration service belongs inuseFactoryinstead. useFactory-
Calls a function and uses its return value. The factory receives, in order, the instances listed in
inject. An entry is either a token (mandatory) or{ token, optional: true }, which passesundefinedwhen nothing is registered under that token, as thelabelparameter shows. The factory itself may beasync(see Asynchronous providers). The result is cached like any singleton, so the factory runs once. useExisting-
Creates an alias for an existing provider.
'AliasedAuditLogger'andAuditLoggerresolve to the same instance, which thesameLogger: trueoutput confirms; this differs fromuseClass, which would create a second instance. The aliased provider must be registered as well.
| Option | Produces | Constructor injection into the produced value |
|---|---|---|
|
The value you give. |
None: Nest does not touch it. |
|
A new instance of the class, once per scope. |
Yes. |
|
The function’s return value (or resolved promise). |
Through the |
|
The instance of another token. |
Not applicable; it reuses the original. |
Providers can carry any value, not only services: the factory form is how configuration objects, connections and clients are usually made available. Never put literal credentials in a provider; read them from the environment or from the configuration module (which Nest provides as ConfigService) inside a factory.
Two further notes. NEST_DEBUG, set to any non-empty string, makes Nest log extra information while it resolves dependencies, which helps when a token cannot be found. And a provider object can also carry a scope (singleton, request or transient) alongside useClass or useFactory; that is covered with the injection scopes.
Exporting custom providers
A custom provider is private to the module that declares it. To let importing modules inject it, export it either by its token or by the full provider object; the two are equivalent.
@Module({
providers: [
DbOptions,
AuditLogger,
clockProvider,
notifierProvider,
connectionProvider,
asyncConnectionProvider,
loggerAliasProvider,
isbnUrlProvider,
CatalogService,
],
// Export by token, or by the full provider object (equivalent).
exports: [CatalogService, 'ASYNC_CONNECTION', connectionProvider, AuditLogger],
})
export class CatalogModule {}
Another module then imports CatalogModule and injects @Inject('ASYNC_CONNECTION') or @Inject(DB_CONNECTION). The connection injected into a consumer in another module is the same instance as the one inside CatalogService. Export a symbol or string token by the same constant you registered it with. Re-exporting a whole module and @Global() are covered in Modules.
Asynchronous providers
When the application must not start accepting requests before an asynchronous task completes (a database connection, a remote configuration fetch), give the provider an async factory. Nest awaits the returned promise before it instantiates any class that injects it.
export const asyncConnectionProvider: Provider = {
provide: 'ASYNC_CONNECTION',
useFactory: async (options: DbOptions) => {
await new Promise((resolve) => setTimeout(resolve, 10)); // pretend to connect
return new DatabaseConnection(options.get().url, 'async');
},
inject: [DbOptions],
};
Consumers inject it like any other token, with @Inject('ASYNC_CONNECTION'). The class receives the resolved value, never a promise.
Source: Asynchronous providers.
Lifecycle and pitfalls:
-
The factories run during
NestFactory.create()(andcompile()in tests), so the awaited work delays startup. That is the point, but it means a slow call needs a timeout. -
If the promise rejects, application startup fails with that error. Nothing is retried; wrap the call yourself if the dependency is flaky.
-
An async factory is awaited before any
onModuleInithook runs (a log shows the factory first, then the hook). Hooks can therefore rely on the resolved value. -
To release a connection created this way on shutdown, do not rely on the factory: wrap the connection in a class that implements
OnApplicationShutdown(and enable shutdown hooks), so the cleanup has a lifecycle hook to run in.
For dependent configuration of whole modules (forRootAsync), see the dynamic modules material in the Fundamentals chapters.
@Optional() dependencies
Mark a parameter with @Optional() when the class has a sensible fallback and the provider may be absent. If nothing is registered under the token, Nest injects undefined instead of failing at startup, and the class must supply the fallback.
@Injectable()
export class BaseClient {
constructor(@Optional() @Inject('HTTP_OPTIONS') readonly options?: HttpOptions) {}
}
Source: Providers, "Optional providers".
@Optional() only covers a missing provider. It does not hide a provider that exists but throws while being built. Because a mistyped token is indistinguishable from an absent one, it also turns a typo into a silent undefined; keep optional dependencies rare and typed T | undefined. In a factory, the equivalent is { token, optional: true } in inject.
The v12 inheritance change
Nest 12 reads the optional markers with Reflect.getOwnMetadata, so a subclass no longer inherits the markers of its parent; the parameter types are still inherited. The migration guide states that a subclass without its own constructor therefore loses the optional status and fails with UnknownDependenciesException, and recommends an explicit constructor:
@Injectable()
export class ExplicitChild extends BaseClient {
constructor(@Optional() @Inject('HTTP_OPTIONS') options?: HttpOptions) {
super(options);
}
}
The 12.1.0 release notes then list a fix, "inherit optional ctor params when the ctor is not redeclared". The behaviour of @nestjs/core 12.1.2 matches the later fix: a subclass that declares no constructor, class ImplicitChild extends BaseClient {}, still resolves with options as undefined, and so does ExplicitChild. In other words, the written migration guide and the 12.1 behaviour differ for the implicit-constructor case. The portable advice is the one the guide gives: when a subclass takes optional dependencies, declare the constructor and repeat @Optional(), which is correct on every v12 release and removes any dependence on that fix. If you have a v12.0.x application, treat the guide as authoritative.
Property-based injection
Besides the constructor, @Inject() can decorate a property. This is useful for a base class, where passing every dependency up through super() from each subclass is tedious:
@Injectable()
export class Greeter {
greet(name: string): string {
return `Hello, ${name}`;
}
}
export abstract class BaseHandler {
@Inject(Greeter)
protected readonly greeter!: Greeter;
}
@Injectable()
export class WelcomeHandler extends BaseHandler {
// No constructor, no super(...) call.
handle(name: string): string {
return this.greeter.greet(name);
}
}
The definite-assignment assertion (!) is needed because the property is assigned by Nest after construction, not in the constructor; strictPropertyInitialization is disabled in the generated project, but the assertion still documents the intent and keeps the class valid under stricter settings. Pitfalls:
-
The property is set after the constructor returns, so it is
undefinedinside the constructor body. Do not read it there. -
Dependencies are invisible in the constructor signature, which makes the class harder to construct by hand in a unit test (Nest’s testing module still injects them).
-
The official guidance is to prefer constructor injection unless the class extends another class.
Manual instantiation
Sometimes you need to leave automatic injection. There are two techniques, both documented separately:
ModuleRef retrieves an existing instance with get() (strict to the current module by default, { strict: false } to search globally), resolves scoped providers with resolve(), and builds a class that is not a provider with create():
@Injectable() // without any decorator the compiler emits no design:paramtypes
export class CoverRenderer {
constructor(readonly greeter: Greeter) {}
}
@Injectable()
export class LazyService {
constructor(private readonly moduleRef: ModuleRef) {}
fromContainer(): Greeter {
return this.moduleRef.get(Greeter);
}
async createUnregistered(): Promise<CoverRenderer> {
return this.moduleRef.create(CoverRenderer);
}
}
Source: Module reference, "Retrieving instances" and Module reference, "Instantiating custom classes dynamically".
The pitfall worth knowing: create(CoverRenderer) returns an object whose greeter is undefined while CoverRenderer carries no decorator, and injects correctly once @Injectable() is added. The compiler emits design:paramtypes only for decorated classes, so create() has nothing to read. get() cannot return request-scoped or transient providers, and create() does not register the class: each call builds a new instance.
A standalone application creates the container without any HTTP server, which lets you obtain providers in a script, a cron job or a command-line tool, or in bootstrap() itself:
const context = await NestFactory.createApplicationContext(DiModule);
const books = context.get(BooksService).findAll();
await context.close();
Source: Standalone applications.
Middleware, guards, interceptors and pipes are not available in that context, because they belong to request handling. For plain new in application code, the right question is whether the object should be a provider: anything with dependencies, shared state or a lifecycle should be one.
Overriding providers in tests
Tokens are what make providers replaceable. The testing module can swap a provider for a test double without touching the code under test:
const moduleRef = await Test.createTestingModule({ imports: [DiModule] })
.overrideProvider(BooksRepository)
.useValue({ findAll: () => [{ id: 9, title: 'Mock' }] })
.compile();
const app = moduleRef.createNestApplication();
await app.init();
await request(app.getHttpServer()).get('/books').expect(200); // [{ id: 9, title: 'Mock' }]
Source: Testing.
Patterns
Singleton. There is no getInstance() to write. Providers are application-wide singletons by default: the container creates one instance per token, in the module that declares it, and shares it. Two lookups return the identical object. The caveat is that state on a singleton is shared by all concurrent requests, so do not keep per-request data in fields. Providers that must be per request use the request scope.
@Injectable()
export class IdGenerator {
private next = 1;
nextId(): number {
return this.next++;
}
}
Factory. A provider can hand out objects that are not providers themselves. Registering the factory function under a token keeps new out of business code and lets the factory use injected collaborators:
export interface Cover {
format: 'svg' | 'png';
render(title: string): string;
}
export type CoverFactory = (format: Cover['format']) => Cover;
export const COVER_FACTORY = Symbol('COVER_FACTORY');
export const coverFactoryProvider: Provider = {
provide: COVER_FACTORY,
useFactory: (ids: IdGenerator): CoverFactory => {
return (format) => ({
format,
render: (title) => `${format}#${ids.nextId()}:${title}`,
});
},
inject: [IdGenerator],
};
A consumer injects the function with @Inject(COVER_FACTORY) private readonly covers: CoverFactory and calls this.covers('svg').render('Dune'), which returned svg#1:Dune. Keep the two ideas apart: useFactory is Nest’s way of building one provider, while the function it returns here is an application-level factory that can be called many times.
Strategy selection. useClass with an abstract class token (the Notifier example above) is the idiomatic way to swap implementations by environment while consumers keep depending on the abstraction.
Diagnosing resolution errors
The commonest startup failure is a missing token. For a class that depends on BooksService in a module that neither declares nor imports it, Nest 12.1.2 printed:
Nest can't resolve dependencies of the Needs (?). Please make sure that the argument BooksService at index [0] is available in the Bad module.
Potential solutions:
- Is Bad a valid NestJS module?
- If BooksService is a provider, is it part of the current Bad?
- If BooksService is exported from a separate @Module, is that module imported within Bad?
@Module({
imports: [ /* the Module containing BooksService */ ]
})
The index tells you which constructor parameter; the module name tells you where to look. The usual causes are:
-
The provider is not in
providersof that module, and the module that has it does not export it, or is not imported. -
A provider class listed in
imports(it belongs inproviders; Nest detects this). -
The same provider declared in two modules instead of one module exporting it.
-
A class injected into itself.
-
Injecting through an interface or a type-only import. When the type is erased the message changes: the argument at the index is reported as "undefined at runtime" and the potential solutions mention
import type. Inject a class, or use a token with@Inject(). -
Two copies of
@nestjs/corein a monorepo, in which case the missing token shown isModuleRef.
Circular constructor dependencies need forwardRef() on both sides, or better a redesign (extract the shared logic into a third provider). They are covered under circular dependency in the Fundamentals chapters. Setting NEST_DEBUG adds per-dependency resolution logs; Nest Devtools can also visualise the graph.
Source: Common errors.
Providers and the request lifecycle
Providers are created at bootstrap, before the first request, and live until shutdown, so they sit outside the per-request pipeline. The pipeline’s own parts are providers in their own right: guards, interceptors, pipes and exception filters are injectable classes. A guard registered through @UseGuards(MyGuard) or an APP_GUARD provider can inject services through its constructor, whereas a guard or pipe registered globally with app.useGlobalGuards(new MyGuard()) is created by hand outside any module and gets no injection. The same applies to global middleware registered with app.use(). The enhancer pages (Guards, Interceptors) and Request Lifecycle cover where each one runs.
Express versus Fastify matters only at the edge. The container, tokens, factories and scopes behave identically, and a controller that depends on BooksService is unchanged when you switch adapters. If a provider needs the underlying HTTP server (for example a catch-all exception filter), inject HttpAdapterHost rather than importing Express or Fastify types, to stay platform-agnostic.
Where the book is outdated
The reference book, published in January 2025 (see the section bibliography), predates Nest 12, so where you follow its dependency-injection chapters:
-
Its examples of optional dependencies and subclasses assume the v11 behaviour of
@Optional()markers being inherited; in v12 declare them on the subclass constructor, as shown above. -
Its project setup is CommonJS-era: in the current ESM template relative imports carry a
.jsextension andimport typecan only be used for types that are not injected (a type-only import of a provider class makes the token disappear at runtime). -
Prefer a
Symboltoken or an abstract class to a bare string where it shows a shared contract; the string tokens in the book work but collide silently. -
Its singleton and factory examples are hand-written patterns; in Nest the singleton is the default scope and the factory is a
useFactoryprovider.
What changed recently
-
Nest 12: optional markers are read with
getOwnMetadata(see The v12 inheritance change); lifecycle hooks of related providers now run in hierarchy order, which matters for initialisation order assumptions between providers. Core packages are ESM-only; a CommonJS application keeps working throughrequire(esm). -
Nest 12.1: a release-note fix re-inherits optional constructor parameters when a subclass does not redeclare its constructor.
-
Documentation: the dependency-injection chapters were rewritten in the September 2026 docs restructure, which added the "Interfaces and abstract classes" guidance used on this page.
Source: Migration guide.