nest_05_modules - Modules & Module Patterns¶
Modules are where NestJS earns its keep Without them your app is a flat namespace where everything imports everything and you can't tell which feature depends on which Modules create boundaries , and boundaries create security - you can lock down what each part of your app can access
what's in here¶
- @Module decorator structure
- Feature modules and encapsulation
- Global modules (use sparingly)
- Dynamic modules with forRoot/forFeature patterns
- Module re-export and shared modules
the @Module decorator¶
Every NestJS app has at least one module - the root AppModule Modules declare what belongs together and what they expose to others
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { UsersRepository } from './users.repository'
@Module({
imports: [], // other modules this module needs
controllers: [UsersController], // routes this module registers
providers: [UsersService, UsersRepository], // DI providers
exports: [UsersService] // providers visible to importing modules
})
export class UsersModule {}
imports- modules whose exported providers become available herecontrollers- route handlers for this featureproviders- services , repositories , factories registered in DIexports- subset of providers that importing modules can inject
feature modules¶
Split your app by domain , not by file type
// src/auth/auth.module.ts
@Module({
imports: [UsersModule], // Auth needs UsersService
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService]
})
export class AuthModule {}
src/
auth/
auth.module.ts
auth.controller.ts
auth.service.ts
strategies/
jwt.strategy.ts
guards/
jwt-auth.guard.ts
users/
users.module.ts
users.controller.ts
users.service.ts
users.repository.ts
dto/
create-user.dto.ts
update-user.dto.ts
Each feature module encapsulates its domain The auth module doesn't need to know how users are stored - it imports UsersModule and uses the exported UsersService
module encapsulation¶
By default , providers in a module are private They can be injected within the module but not by external modules Only providers listed in exports: [] are visible outside
@Module({
providers: [
UsersService, // private - only this module can use it
UsersRepository, // private
EmailService // private
],
exports: [UsersService] // only UsersService is visible to importing modules
})
export class UsersModule {}
This encapsulation is your security boundary If someone accidentally tries to inject UsersRepository in the AuthModule , Nest throws at compile time - not at 3AM in production when an auth bypass exploits a leaked repository
global modules¶
Sometimes you need a provider available everywhere without importing its module in every feature ConfigService is the classic example
import { Module, Global } from '@nestjs/common'
import { ConfigService } from './config.service'
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService]
})
export class ConfigModule {}
With @Global() , ConfigService is available in every module without importing ConfigModule Use globals sparingly - they're convenient but they break the explicit dependency tracking that makes NestJS modular Good candidates: config , logging , database connections Bad candidates: business logic services
dynamic modules¶
Dynamic modules accept configuration at import time
import { Module, DynamicModule } from '@nestjs/common'
@Module({})
export class DatabaseModule {
static forRoot(config: DatabaseConfig): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: config
},
DatabaseService
],
exports: [DatabaseService]
}
}
static forFeature(entity: Function): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'ENTITY_REPOSITORY',
useFactory: (connection: Connection) =>
connection.getRepository(entity),
inject: ['DATABASE_CONNECTION']
}
],
exports: ['ENTITY_REPOSITORY']
}
}
}
Usage:
@Module({
imports: [
DatabaseModule.forRoot({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432')
})
]
})
export class AppModule {}
@Module({
imports: [
DatabaseModule.forFeature(User)
],
providers: [UsersService]
})
export class UsersModule {}
forRoot() sets up the module globally - called once in AppModule forFeature() configures per-feature functionality - called in each feature module This pattern comes from @nestjs/typeorm , @nestjs/mongoose , and @nestjs/config
module re-export¶
Export modules to make their exports transitively available:
@Module({
imports: [CommonModule],
exports: [CommonModule] // re-export CommonModule's exports
})
export class SharedModule {}
Modules importing SharedModule also get access to CommonModule's exports Useful for creating "barrel" modules that group related functionality
multi-module architecture for security¶
app.module.ts
imports:
ConfigModule.forRoot({ isGlobal: true })
CoreModule // guards , interceptors , filters
AuthModule // auth strategies , guards
UsersModule // user CRUD
AdminModule // admin-only features
HealthModule // public health check (no auth)
The AdminModule imports AuthModule and uses guards to restrict access The HealthModule imports nothing auth-related - it can't accidentally expose admin data Module boundaries create clear attack surface visibility
prerequisites¶
nest_04_providers - Providers & Dependency Injection
next -> nest_06_middleware - Middleware