Skip to content

NestJS Dependency Injection

Dependency injection (DI) is the mechanism that powers everything in NestJS, wiring controllers, services, and other providers together automatically. This lesson explains how Nest's DI container actually works.

How NestJS Resolves Dependencies

When Nest builds a module, it inspects every provider's constructor using TypeScript's emitted type metadata. For each constructor parameter, it looks up a matching provider (by class reference or injection token) and either reuses an existing instance or constructs a new one, recursively resolving that provider's own dependencies first.

This means you rarely instantiate classes with new in a Nest application, the framework's IoC (Inversion of Control) container does it for you based purely on constructor type signatures.

@Injectable()
export class CatsRepository { /* ... */ }

@Injectable()
export class CatsService {
  constructor(private readonly repo: CatsRepository) {}
}

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
}

Nest resolves CatsRepository first, injects it into CatsService, then injects the resulting CatsService instance into CatsController, all without any manual wiring code.

Injection Tokens

{
  provide: 'CATS_REPOSITORY',   // token
  useClass: CatsRepository,     // implementation
}

@Inject('CATS_REPOSITORY') private readonly repo: CatsRepository
  • By default, a class itself acts as its own injection token.
  • Custom tokens (strings or symbols) let you inject interfaces, values, or swappable implementations.
  • @Inject(token) is required whenever the token isn't simply the class type.
  • Providers can depend on other providers by token just as easily as by class.

Dependency Injection Cheatsheet

How different provider shapes get injected.

Pattern Example
Class provider (implicit token) constructor(private svc: CatsService)
Custom token with @Inject @Inject('CONFIG') private config: AppConfig
Optional dependency @Optional() @Inject('X') private x?: X
Injecting an array of providers @Inject('STRATEGIES') private strategies: Strategy[]

Constructor Injection vs Property Injection

NestJS favors constructor injection, dependencies are declared as typed constructor parameters, making them explicit, immutable (with readonly), and easy to spot when reading a class. Property injection with @Inject() on a class field exists but is used far less often, typically only to resolve circular dependencies.

@Injectable()
export class CatsService {
  @Inject(forwardRef(() => OwnersService))
  private ownersService: OwnersService;
}

Property injection combined with forwardRef() is one common way to break a circular dependency between two services that need each other.

Why DI Makes Testing Easier

Because dependencies are resolved by the container rather than hardcoded, unit tests can build a TestingModule and swap any provider for a mock or stub, no monkey-patching or module-mocking tricks required.

Common Mistakes

  • Forgetting @Inject(token) when a provider uses a custom string or symbol token.
  • Creating circular dependencies between two services without forwardRef().
  • Instantiating a service manually with new, bypassing the DI container and losing its own injected dependencies.
  • Confusing a class used purely as a TypeScript interface (erased at runtime) with an actual injectable provider.

Key Takeaways

  • Nest's IoC container resolves constructor dependencies automatically based on type or token.
  • Constructor injection is the standard pattern; property injection is mostly for edge cases like circular deps.
  • Custom injection tokens let you inject values, factories, or swappable interface implementations.
  • DI is what makes Nest's testing utilities able to swap real providers for mocks so easily.

Pro Tip

If you see a circular dependency error at startup, first ask whether the two services truly need each other. Often the fix is extracting shared logic into a third service rather than reaching for forwardRef().