Providers and dependency injection
How Nest constructs your objects, and the injection patterns worth knowing beyond the constructor.
Dependency injection is Nest's core mechanism. You declare what a class needs; Nest works out how to build it.
@Injectable()
export class OrdersService {
constructor(
private readonly users: UsersService,
private readonly prisma: PrismaService,
) {}
}Nest reads the parameter types at runtime (via emitDecoratorMetadata), looks up a provider for each, constructs them if they do not exist yet, and passes them in. There is no new anywhere.
The payoff is testing:
const module = await Test.createTestingModule({
providers: [
OrdersService,
{ provide: UsersService, useValue: { findOne: jest.fn() } },
{ provide: PrismaService, useValue: prismaMock },
],
}).compile();Swapping a real dependency for a fake is a one-line change, because nothing ever hardcoded the dependency.
The four provider forms#
// 1. class — the common case
providers: [UsersService]
// 2. useClass — swap the implementation behind a token
providers: [{ provide: MailerService, useClass: process.env.NODE_ENV === 'test' ? FakeMailer : SesMailer }]
// 3. useValue — a constant or a mock
providers: [{ provide: 'CONFIG', useValue: { retries: 3 } }]
// 4. useFactory — computed, possibly async, may inject
providers: [{
provide: 'DB',
useFactory: async (config: ConfigService) => createPool(config.get('DATABASE_URL')),
inject: [ConfigService],
}]String tokens and how to avoid them#
Interfaces do not exist at runtime, so you cannot inject by interface. The workaround is a token — but use a typed constant, not a bare string.
export const MAILER = Symbol('MAILER');
export interface Mailer {
send(to: string, subject: string, body: string): Promise<void>;
}providers: [{ provide: MAILER, useClass: SesMailer }]
constructor(@Inject(MAILER) private readonly mailer: Mailer) {}A Symbol cannot collide and cannot be typo'd into a different provider.
Scopes#
Providers are singletons by default, and that is almost always what you want.
@Injectable({ scope: Scope.REQUEST }) // new instance per request
export class RequestContext {}Optional and circular#
constructor(@Optional() private readonly cache?: CacheService) {}
// two services that need each other
constructor(@Inject(forwardRef(() => OrdersService)) private orders: OrdersService) {}As with modules, forwardRef between services usually means a third thing wants extracting.
Common questions#
Why can I not inject an interface?#
TypeScript interfaces are erased at compile time, so there is nothing at runtime for Nest to look up. Use a Symbol token with @Inject(), and keep the interface for the type annotation — you get both the abstraction and working injection.
When should I use useFactory?#
When constructing the provider needs async work or configuration — a database pool, an SDK client built from environment values. For a plain class with injectable dependencies, the class form is simpler and does the same thing.
Does dependency injection slow things down?#
Only at startup, and imperceptibly: the graph is constructed once. The exception is request-scoped providers, which construct per request and can be measurably expensive on a hot path.
Get the NestJS agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for NestJS. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.