What are Dynamic Modules in NestJS and when to use them?
Dynamic Modules in NestJS
Dynamic modules allow you to create configurable, reusable modules that accept options at import time. This is how libraries like @nestjs/jwt, @nestjs/bull, and TypeOrmModule work.
Static vs Dynamic Modules
Static Module
typescript
@Module({
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
// Usage — no configuration
@Module({
imports: [CatsModule],
})
export class AppModule {}Dynamic Module
typescript
// Usage — with configuration
@Module({
imports: [
DatabaseModule.forRoot({ host: 'localhost', port: 5432 }),
],
})
export class AppModule {}Creating a Dynamic Module
Basic Pattern: forRoot / forFeature
typescript
export interface DatabaseModuleOptions {
host: string;
port: number;
username: string;
password: string;
database: string;
}
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseModuleOptions): DynamicModule {
return {
module: DatabaseModule,
global: true, // Available everywhere
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
{
provide: 'DATABASE_CONNECTION',
useFactory: async (opts: DatabaseModuleOptions) => {
return createConnection(opts);
},
inject: ['DATABASE_OPTIONS'],
},
DatabaseService,
],
exports: ['DATABASE_CONNECTION', DatabaseService],
};
}
}Async Configuration (forRootAsync)
For when options come from environment or other services:
typescript
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseModuleOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{ provide: 'DATABASE_OPTIONS', useValue: options },
DatabaseService,
],
exports: [DatabaseService],
};
}
static forRootAsync(options: {
imports?: any[];
useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions;
inject?: any[];
}): DynamicModule {
return {
module: DatabaseModule,
imports: options.imports || [],
providers: [
{
provide: 'DATABASE_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}Usage:
typescript
@Module({
imports: [
DatabaseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}forFeature Pattern
For module-level configuration (like TypeORM entities per module):
typescript
@Module({})
export class StorageModule {
static forRoot(options: StorageOptions): DynamicModule {
return {
module: StorageModule,
global: true,
providers: [
{ provide: 'STORAGE_OPTIONS', useValue: options },
StorageService,
],
exports: [StorageService],
};
}
static forFeature(bucket: string): DynamicModule {
return {
module: StorageModule,
providers: [
{ provide: 'STORAGE_BUCKET', useValue: bucket },
{
provide: `STORAGE_${bucket.toUpperCase()}`,
useFactory: (storageService: StorageService) => {
return storageService.getBucket(bucket);
},
inject: [StorageService],
},
],
exports: [`STORAGE_${bucket.toUpperCase()}`],
};
}
}typescript
// In feature modules
@Module({
imports: [StorageModule.forFeature('avatars')],
})
export class UsersModule {}
@Module({
imports: [StorageModule.forFeature('documents')],
})
export class DocumentsModule {}Real-World Example: Email Module
typescript
@Module({})
export class EmailModule {
static forRoot(options: EmailModuleOptions): DynamicModule {
return {
module: EmailModule,
global: true,
providers: [
{ provide: 'EMAIL_OPTIONS', useValue: options },
EmailService,
],
exports: [EmailService],
};
}
static forRootAsync(options: EmailAsyncOptions): DynamicModule {
return {
module: EmailModule,
global: true,
imports: options.imports || [],
providers: [
{
provide: 'EMAIL_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
EmailService,
],
exports: [EmailService],
};
}
}Common Patterns Summary
| Method | Use Case |
|---|---|
forRoot() | Global configuration (once in AppModule) |
forRootAsync() | Async config (from ConfigService, etc.) |
forFeature() | Per-module configuration |
register() | Same as forRoot but can be called multiple times |
registerAsync() | Async version of register |
Key insight: Dynamic modules are NestJS's plugin system. Use
forRootfor one-time global setup,forFeaturefor per-module customization. The async variants let you use dependency injection to resolve configuration values.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.