Modules
Modules are the organizational backbone of a NestJS application. Every Nest app has at least one, the root module, and Nest uses modules to build the dependency graph that wires controllers and providers together. Thinking in modules is thinking in cohesive, loosely coupled features.
The @Module decorator
A module is a class annotated with @Module(). The decorator takes a metadata object with four properties, each defining a different aspect of the module’s surface.
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
imports: [],
exports: [],
})
export class CatsModule {}
| Property | Meaning |
|---|---|
controllers | Controllers instantiated by this module |
providers | Providers available for injection within this module |
imports | Other modules whose exported providers this module needs |
exports | The subset of this module’s providers visible to importers |
A provider declared in a module is private to that module by default. To share it, you must explicitly add it to
exports. This encapsulation is what keeps large applications decoupled.
The root module
AppModule is the module passed to NestFactory.create(). It composes the application by importing every feature module.
@Module({
imports: [CatsModule, UsersModule, AuthModule],
})
export class AppModule {}
Feature modules
A feature module groups everything related to one domain concept, its controller, service, DTOs, and entities, under a single directory and module. This is the recommended way to organize anything non-trivial.
src/
├── cats/
│ ├── dto/
│ ├── cats.controller.ts
│ ├── cats.service.ts
│ └── cats.module.ts
└── app.module.ts
The feature module exports only what other modules legitimately need:
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService], // now importable elsewhere
})
export class CatsModule {}
Any module that imports CatsModule can inject CatsService, but cannot reach CatsService’s private collaborators. Encapsulation is preserved.
Shared modules
When a provider is needed across many features, database access, configuration, logging, place it in a shared module and export it. Importing modules receive the same singleton instance, because Nest caches a module once it is resolved.
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class DatabaseModule {}
@Module({
imports: [DatabaseModule], // reuses the single PrismaService
providers: [CatsService],
})
export class CatsModule {}
Mark cross-cutting modules
@Global()only sparingly. A global module’s exports become available everywhere without an explicit import, convenient, but it hides dependencies and makes the graph harder to reason about. Explicit imports are almost always better.
Dynamic modules
Static modules expose a fixed set of providers. Sometimes a module must be configured by the consumer, think a database module that needs a connection string, or a cache module that needs a TTL. Dynamic modules solve this by exposing a static method that returns the module definition at runtime.
import { DynamicModule, Module } from '@nestjs/common';
@Module({})
export class ConfigModule {
static forRoot(options: ConfigOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{ provide: 'CONFIG_OPTIONS', useValue: options },
ConfigService,
],
exports: [ConfigService],
global: true,
};
}
}
Consumers configure it where they import it:
@Module({
imports: [ConfigModule.forRoot({ folder: './config' })],
})
export class AppModule {}
The forRoot / forFeature naming convention is the de facto standard across the Nest ecosystem (TypeOrmModule.forRoot(), JwtModule.register()), use it for your own configurable modules so they feel idiomatic.
Best Practices
- One feature, one module. Co-locate the controller, service, and DTOs for a domain in a dedicated module.
- Export deliberately. Only expose providers that other modules genuinely consume; everything else stays private.
- Prefer explicit imports over
@Global(). Global modules obscure the dependency graph; use them only for truly ubiquitous concerns. - Use the
forRoot/forFeatureconvention for dynamic modules so they match ecosystem expectations. - Keep the root module thin, it should mostly import feature modules, not declare providers itself.