This chapter follows Controllers in NestJS.
A provider is anything Nest can inject: most often a service class, but also a repository, a client for another API, a factory, or a plain configuration value. Classes that use a provider never create it. They declare it as a constructor parameter, and Nest’s injector supplies an instance. That inversion is what lets a test hand a controller a fake service, and what lets one database connection be shared by every feature.
A service as a provider
nest g service cats creates a class marked with @Injectable(). The decorator lets Nest read the class’s constructor parameter types, so the class can itself receive dependencies.
import { Injectable, NotFoundException } from '@nestjs/common';import { UsersService } from '../users/users.service';import { CreateCatDto } from './dto/create-cat.dto';import { Cat } from './cat.entity';
@Injectable()export class CatsService { private readonly cats: Cat[] = [];
constructor(private readonly usersService: UsersService) {}
create(dto: CreateCatDto): Cat { const cat = { id: this.cats.length + 1, ...dto }; this.cats.push(cat); return cat; }
findOne(id: number): Cat { const cat = this.cats.find((c) => c.id === id); if (!cat) throw new NotFoundException(`Cat ${id} not found`); return cat; }}Two declarations are needed for the injection to work: CatsService appears in the providers of CatsModule, and UsersService is exported by a module that CatsModule imports. Modules in NestJS explains both.
How the injector resolves a dependency
Every provider is registered under a token. For a class, the token is the class itself. When Nest creates CatsService, it reads the parameter types that TypeScript emitted for the constructor, finds UsersService, looks up a provider with that token in the module’s scope, creates it first if needed, and passes it in.
flowchart LR ctrl[CatsController] -->|needs| catsTok[CatsService token] catsTok --> catsInst[CatsService instance] catsInst -->|needs| usersTok[UsersService token] usersTok --> usersInst[UsersService instance]
The lookup happens once, at startup. By default each provider is a singleton: one instance per application, shared by everyone who injects it. Because the constructor types are the lookup keys, injecting an interface does not work. Interfaces disappear at compile time and leave nothing to look up. Use a class, or a custom token as in the next section.
Custom providers
The shorthand providers: [CatsService] stands for { provide: CatsService, useClass: CatsService }. The long form separates the token from what is created, which gives four options:
import { Module } from '@nestjs/common';import { ConfigService } from '@nestjs/config';
export const CLOCK = Symbol('CLOCK');export const CACHE_TTL = 'CACHE_TTL';
@Module({ providers: [ { provide: CatsRepository, useClass: InMemoryCatsRepository }, { provide: CLOCK, useValue: { now: () => new Date() } }, { provide: CACHE_TTL, useFactory: (config: ConfigService) => config.get<number>('CACHE_TTL', 60), inject: [ConfigService], }, { provide: AppLogger, useExisting: PinoLogger }, ],})export class CatsModule {}useClasscreates a different class for the token, for example an in-memory repository in development and a database one in production.useValuehands over an existing object as is. Tests use it to replace a service with a mock.useFactorycalls a function, which may beasync, and injects the result.injectlists the factory’s own dependencies.useExistingmakes a second token point at a provider that already exists, without creating another instance.
A token that is not a class needs @Inject() at the injection site, because there is no type to read:
constructor( @Inject(CLOCK) private readonly clock: { now(): Date }, @Inject(CACHE_TTL) private readonly cacheTtl: number,) {}@Optional() next to @Inject() lets the dependency be undefined when no provider is registered, instead of failing at startup.
Injection scopes
The scope decides how long an instance lives:
| Scope | Instance | Typical use |
|---|---|---|
DEFAULT |
One per application | Almost everything |
REQUEST |
One per incoming request | Data tied to one request, such as a tenant ID |
TRANSIENT |
One per consumer that injects it | A logger that records which class owns it |
import { Inject, Injectable, Scope } from '@nestjs/common';import { REQUEST } from '@nestjs/core';import type { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })export class TenantContext { constructor(@Inject(REQUEST) private readonly request: Request) {}
get tenantId(): string | undefined { return this.request.header('x-tenant-id'); }}Request scope bubbles up: a singleton that injects TenantContext becomes request-scoped too, and so does every controller above it. Nest then creates that whole chain on every request, which costs memory and time. Prefer passing request data as method arguments, and reach for request scope only when that is impractical.
Circular dependencies
When CatsService injects UsersService and UsersService injects CatsService, neither can be created first. Nest reports the cycle at startup. forwardRef() defers one side of the lookup:
constructor( @Inject(forwardRef(() => CatsService)) private readonly catsService: CatsService,) {}The same wrapper exists for modules that import each other: imports: [forwardRef(() => CatsModule)]. Treat it as a stopgap. A cycle usually means some logic belongs in a third provider that both sides can depend on.
Next: The NestJS request lifecycle.
Check your understanding
Question 1 of 5
0 of 5 checked