NestJS — Enterprise Node.js

Dependency Injection Deep Dive

16 min Lesson 7 of 48

Dependency Injection Deep Dive

You have used dependency injection already — declaring a service in a constructor and letting NestJS supply it. Now let us open the box. Understanding the IoC container and provider tokens turns DI from magic into a tool you fully control.

The IoC container

IoC stands for Inversion of Control. Instead of your classes creating their own dependencies, a central container creates and supplies them. At startup, NestJS scans every module, builds a map of providers, then instantiates them in the right order, injecting each one's dependencies.

You describe, the container builds. You never write new UsersService(new Database(...)). You declare what each class needs, and the container assembles the whole graph for you.

Provider tokens

Every provider is registered under a token — a key the container uses to find it. With the standard shorthand, the token is the class itself:

providers: [UsersService] // is shorthand for: providers: [{ provide: UsersService, useClass: UsersService }]

So when a constructor asks for UsersService, the container looks up the token UsersService and injects the matching instance. The class doubles as both the type and the lookup key.

Injecting by a non-class token

Sometimes there is no class to use as a token — for example a configuration object or a plain value. You register it under a string or symbol token and inject it with @Inject():

import { Injectable, Inject } from '@nestjs/common'; // registration providers: [{ provide: 'API_KEY', useValue: 'secret-123' }] // injection @Injectable() export class PaymentService { constructor(@Inject('API_KEY') private readonly apiKey: string) {} }
Prefer exported constants over raw strings for tokens (e.g. export const API_KEY = 'API_KEY'). It avoids typos and lets your editor find every usage.

Optional dependencies

If a dependency may be absent, mark it @Optional() so the container injects undefined instead of throwing:

import { Injectable, Optional, Inject } from '@nestjs/common'; @Injectable() export class HttpService { constructor(@Optional() @Inject('HTTP_OPTIONS') private options?: any) {} }

Circular dependencies

If two providers depend on each other, the container cannot decide which to build first. NestJS resolves this with forwardRef(), which defers the reference until both exist:

@Injectable() export class UsersService { constructor( @Inject(forwardRef(() => AuthService)) private readonly authService: AuthService, ) {} }
A circular dependency is usually a design smell. Before reaching for forwardRef(), ask whether the shared logic belongs in a third service both can depend on. Use forwardRef() as a last resort, not a habit.

Summary

NestJS's IoC container builds your dependency graph from provider tokens. With the class shorthand the class is the token; for values and interfaces you register under a string/symbol token and inject with @Inject(). @Optional() allows missing dependencies, and forwardRef() handles unavoidable circular references. Next we explore the four ways to define a provider.