Modules and Dependency Injection
@Module(), how Nest's DI container resolves dependencies, and organizing a larger app into feature modules.
The @Module() decorator
A module is a class annotated with @Module() that groups related controllers and providers together and declares how it relates to other modules. Every Nest application has at least one module — the root AppModule — and larger applications are organized into many feature modules.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
The @Module() decorator accepts four keys:
| Key | Purpose |
|---|---|
controllers |
Controllers instantiated as part of this module |
providers |
Providers instantiated by this module's DI container, available within it |
imports |
Other modules whose exported providers this module needs |
exports |
Which of this module's own providers are made available to modules that import it |
The root module ties everything together:
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [UsersModule, OrdersModule],
})
export class AppModule {}
How the DI container resolves dependencies
When the application bootstraps with NestFactory.create(AppModule), Nest walks the module graph starting at AppModule, and for every provider and controller it finds, inspects the constructor to see what it needs. Each dependency is looked up by its type (used as a DI "token"), instantiated if it hasn't been already, and injected — recursively, so a service that itself depends on another service gets that dependency resolved first.
By default every provider is a singleton scoped to the whole application: the first time it's needed, Nest constructs exactly one instance and reuses it for every subsequent injection, for the lifetime of the app. Nest also supports request-scoped and transient providers for less common cases, at some performance cost, since those disable the reuse a singleton gives you.
If Nest cannot find a provider a class depends on — because it was never added to a providers array, or the module that exports it was never imported — the application fails to start with an error naming exactly which dependency it couldn't resolve.
Feature modules for organizing a larger app
As an application grows, grouping everything into one giant module defeats the purpose of modularity. The convention is one module per feature/domain, each owning its own controller(s) and service(s):
src/
├── app.module.ts
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ └── users.service.ts
└── orders/
├── orders.module.ts
├── orders.controller.ts
└── orders.service.ts
If OrdersModule needs to look up a user while creating an order, UsersModule must explicitly export UsersService, and OrdersModule must import UsersModule:
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // makes UsersService available to modules that import UsersModule
})
export class UsersModule {}
// orders.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
@Module({
imports: [UsersModule],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}
// orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class OrdersService {
constructor(private readonly usersService: UsersService) {}
createOrder(userId: number, item: string) {
const user = this.usersService.findOne(userId);
return { user, item };
}
}
Note that OrdersService never imports UsersModule itself — it only depends on UsersService. It's OrdersModule (the module, not the service) that declares the imports: [UsersModule] relationship; Nest's DI container then makes UsersService available for injection anywhere inside OrdersModule.
Common mistakes
- Forgetting to add a provider to its module's
providersarray — a class marked@Injectable()still isn't usable anywhere until some module declares it as a provider. - Forgetting
exports— a module importingUsersModulecan only inject providersUsersModuleexplicitly exported, not every provider it happens to declare internally. - Creating a circular dependency between two modules that both need something from each other. Nest supports resolving this with
forwardRef(), but a genuine circular dependency between two feature modules is often a sign that some shared logic should be extracted into a third module both of them import instead.