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.

Typescript
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:

Typescript
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):

Plaintext
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:

Typescript
// 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 {}
Typescript
// 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 {}
Typescript
// 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 providers array — a class marked @Injectable() still isn't usable anywhere until some module declares it as a provider.
  • Forgetting exports — a module importing UsersModule can only inject providers UsersModule explicitly 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.