Foundations View as Markdown

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.

ts
@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:

ts
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#

ts
// 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.

src/mail/mail.tokens.ts
export const MAILER = Symbol('MAILER');

export interface Mailer {
  send(to: string, subject: string, body: string): Promise<void>;
}
ts
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.

ts
@Injectable({ scope: Scope.REQUEST })     // new instance per request
export class RequestContext {}

Optional and circular#

ts
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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.