Skip to main content
Practice Problems

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

MethodUse 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 forRoot for one-time global setup, forFeature for per-module customization. The async variants let you use dependency injection to resolve configuration values.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems