Suggest an editImprove this articleRefine the answer for “What are Dynamic Modules in NestJS and when to use them?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Dynamic module in NestJS** is a module that returns a `DynamicModule` object from a static method like `forRoot()`, letting callers pass configuration at import time. ```typescript @Module({}) export class LoggerModule { static forRoot(level: 'error' | 'debug'): DynamicModule { return { module: LoggerModule, providers: [{ provide: 'LOG_LEVEL', useValue: level }, LoggerService], exports: [LoggerService], }; } } ``` **Key point:** use dynamic modules when configuration varies between environments or app instances. For providers that never need configuration, a static module is enough.Shown above the full answer for quick recall.Answer (EN)Image**Dynamic module in NestJS** is a module that returns a customized `DynamicModule` object from a static method like `forRoot()`, letting you pass configuration at import time instead of hardcoding it. ## Theory ### TL;DR - Think of a static module as a pre-built LEGO set. A dynamic one is a factory kit: you send instructions, you get a set tailored to your build. - Static modules export a fixed `@Module({})` decorator with the same providers every time. Dynamic modules generate that metadata on demand based on the options you pass in. - Use dynamic if config varies per app or environment (DB credentials, JWT secret). Stick to static for shared logic that never needs configuration. - Standard naming: `forRoot()` for global one-time setup, `forFeature()` for per-module customization, `forRootAsync()` when options come from another service. - The return object must always include a `module` key. Forgetting it breaks the DI graph in a way that is surprisingly hard to trace. ### Quick example ```typescript // Static: always the same providers @Module({ providers: [LoggerService] }) export class LoggerModule {} // Dynamic: providers change based on what you pass in @Module({}) export class LoggerModule { static forRoot(level: 'error' | 'debug' | 'verbose'): DynamicModule { return { module: LoggerModule, providers: [ { provide: 'LOG_LEVEL', useValue: level }, // value injected from outside LoggerService, ], exports: [LoggerService], }; } } // Usage @Module({ imports: [LoggerModule.forRoot('debug')], }) export class AppModule {} ``` `LoggerModule.forRoot('debug')` runs at import time and returns a plain object that NestJS treats as module metadata. No magic. The `LOG_LEVEL` token is now available for injection anywhere in the module. ### Static vs dynamic modules Static modules export a fixed `@Module({})` decorator with hardcoded providers. Every import gets the identical setup, which works fine for a shared utility service with no configuration. Dynamic modules call a static method that returns a `DynamicModule` object, injecting options at registration time. This lets you create different configurations of the same module in different parts of the app, which solves shared-state problems in multi-tenant or configurable libraries. One practical consequence: with static modules, there is no clean place to put DB credentials. You either hardcode them or reach for globals. Dynamic modules give you a proper API for that. ### When to use - Config differs per app or environment: `forRoot()` with an options object (host, port, credentials) - Options come from another service like `ConfigService`: `forRootAsync()` with a factory function and an `inject` array - Per-module entity or queue setup: `forFeature()`, for example `TypeOrmModule.forFeature([User])` - Shared service with no configuration needed: a static module is enough - Testing with mock configuration: dynamic modules let you pass fake options without touching source code ### How NestJS processes dynamic modules When `NestModuleLoader.create()` runs during bootstrap, it scans every entry in `imports[]`. If the entry is a function call like `LoggerModule.forRoot('debug')`, that call executes synchronously and returns a plain `DynamicModule` object. NestJS then merges that object's `providers`, `imports`, and `exports` into the global metadata graph stored in `ModulesContainer`. All of this happens before any providers resolve. After bootstrap, there is no additional overhead as providers resolve lazily through the DI container. `forRootAsync()` works differently. NestJS registers the `useFactory` provider normally but delays calling the factory until all dependencies listed in `inject` are available. ### Common mistakes **Forgetting `module` in the return object** ```typescript // Wrong static forRoot(opts: Options) { return { providers: [{ provide: 'CONFIG', useValue: opts }] }; // no module key } // Right static forRoot(opts: Options): DynamicModule { return { module: MyModule, // required providers: [{ provide: 'CONFIG', useValue: opts }], exports: ['CONFIG'], }; } ``` NestJS cannot register metadata without the `module` key. The error message ("Nest can't resolve dependencies") typically points somewhere else, which makes this one of those bugs that wastes 20 minutes because the stack trace never points directly at the cause. **Mutating the options object** ```typescript // Wrong static forRoot(opts: Options) { opts.host = 'overridden'; // mutates the caller's object return { module: MyModule, providers: [{ provide: 'CONFIG', useValue: opts }] }; } // Right static forRoot(opts: Options): DynamicModule { return { module: MyModule, providers: [{ provide: 'CONFIG', useValue: { ...opts } }], // spread or deep clone }; } ``` If two parts of the app call `forRoot()` with different options, mutation causes them to interfere with each other. Spread or deep-clone the options before using them. **Putting exports in the class decorator instead of the return object** ```typescript // Wrong @Module({ exports: [Service] }) // fires even without forRoot being called export class MyModule { static forRoot(): DynamicModule { return { module: MyModule }; // no exports here } } // Right @Module({}) export class MyModule { static forRoot(): DynamicModule { return { module: MyModule, providers: [Service], exports: [Service] }; } } ``` The class-level decorator runs unconditionally. Modules that never call `forRoot()` still get those exports, which leads to subtle bugs in multi-tenant setups. **Using `forRoot` inside feature modules** `forRoot` is for global root configuration, called once in AppModule. Calling it inside a feature module creates duplicate providers in the DI container. Feature modules should use `forFeature()` instead. **Forgetting `inject` in `forRootAsync`** ```typescript // Wrong - config is undefined at runtime TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => config.getDbOptions(), // inject is missing }); // Right TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => config.getDbOptions(), inject: [ConfigService], // required, must match factory parameter order }); ``` This is the most common mistake among developers new to async configuration. The factory receives `undefined` arguments and throws at runtime. TypeScript won't catch it at compile time. ### Real-world usage - `@nestjs/typeorm`: `TypeOrmModule.forRoot({ host, entities })` in AppModule, `TypeOrmModule.forFeature([User])` in each feature module - `@nestjs/jwt`: `JwtModule.register({ secret })` for auth token signing - `@nestjs/bull`: `BullModule.forRoot({ redis })` for queue setup, `BullModule.forFeature(Processor)` per job type - `@nestjs/config`: `ConfigModule.forRoot()` loads `.env` globally - `@nestjs/mongoose`: `MongooseModule.forRoot('mongodb://...')`, then `MongooseModule.forFeature([Cat])` per module Every major NestJS integration library follows this pattern. Once you understand how dynamic modules work, reading the source of any of these becomes straightforward. ### Follow-up questions **Q:** What is the return type of `forRoot()` and what fields does it accept? **A:** It returns the `DynamicModule` interface: `{ module: Type<any>, providers?: Provider[], imports?: any[], exports?: any[], global?: boolean }`. The `module` field is the only required one. **Q:** What is the difference between `forRoot()` and `forRootAsync()`? **A:** `forRoot()` takes synchronous options directly as arguments. `forRootAsync()` takes a `useFactory` function with an `inject` array, so NestJS resolves the listed dependencies first and passes them into the factory. **Q:** How does NestJS handle two `forRoot()` calls for the same module? **A:** It merges providers into a single module instance. Set `global: true` for a shared global configuration. If you need isolated instances per module, use `forFeature()` instead. **Q:** Can a dynamic module include other dynamic modules in its `imports`? **A:** Yes. You can nest `forFeature()` calls inside a `forRoot()` return's `imports` array. This is exactly how TypeORM chains entity repositories inside the root connection module. **Q:** (Senior) What happens if `forRoot()` throws during bootstrap, and how do you debug it? **A:** The app fails to start with a bootstrap exception. Run `nest start --debug` and trace the stack from `ModulesContainer.register()`. Add a `console.log` before the return statement in your dynamic method to inspect what object NestJS actually receives. Circular imports between modules that both use `forRootAsync` are another common cause of silent bootstrap failures. **Q:** When would you skip dynamic modules entirely and use a plain factory provider? **A:** For one-off setups that are not meant to be reused across projects. If you have one app and one DB, a `useFactory` provider directly in `AppModule` is simpler and completely valid. Dynamic modules add API design overhead that only pays off when the module is shared or reused. ## Examples ### Basic: configurable logger module ```typescript import { Module, DynamicModule } from '@nestjs/common'; type LogLevel = 'error' | 'debug' | 'verbose'; @Module({}) export class LoggerModule { static forRoot(level: LogLevel): DynamicModule { return { module: LoggerModule, providers: [ { provide: 'LOG_LEVEL', useValue: level }, LoggerService, // uses @Inject('LOG_LEVEL') in its constructor ], exports: [LoggerService], }; } } @Module({ imports: [LoggerModule.forRoot('debug')], }) export class AppModule {} ``` `LoggerService` receives the `'debug'` level through its constructor via `@Inject('LOG_LEVEL')`. Change the argument to `'error'` in production without touching any service code. ### Intermediate: database module with async config ```typescript import { Module, DynamicModule } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Module({}) export class DatabaseModule { static forRootAsync(options: { imports?: any[]; useFactory: (...args: any[]) => Promise<{ host: string; port: number }>; inject?: any[]; }): DynamicModule { return { module: DatabaseModule, imports: options.imports || [], providers: [ { provide: 'DB_CONFIG', useFactory: options.useFactory, inject: options.inject || [], }, DatabaseService, ], exports: [DatabaseService], }; } } // Usage in AppModule @Module({ imports: [ ConfigModule.forRoot(), DatabaseModule.forRootAsync({ imports: [ConfigModule], useFactory: (config: ConfigService) => ({ host: config.get<string>('DB_HOST'), port: config.get<number>('DB_PORT'), }), inject: [ConfigService], // must match useFactory parameter order }), ], }) export class AppModule {} ``` `DB_CONFIG` resolves only after `ConfigService` is ready. Remove the `inject` array and `config` becomes `undefined` at runtime. TypeScript does not catch this because the factory signature accepts `any[]`. ### Advanced: forFeature for per-module storage buckets ```typescript @Module({}) export class StorageModule { static forRoot(options: { provider: string; region: string }): DynamicModule { return { module: StorageModule, global: true, // one shared StorageService across the app providers: [ { provide: 'STORAGE_OPTIONS', useValue: { ...options } }, StorageService, ], exports: [StorageService], }; } static forFeature(bucket: string): DynamicModule { return { module: StorageModule, providers: [ { provide: `STORAGE_BUCKET_${bucket.toUpperCase()}`, useFactory: (storage: StorageService) => storage.getBucket(bucket), inject: [StorageService], // pulls from global StorageService }, ], exports: [`STORAGE_BUCKET_${bucket.toUpperCase()}`], }; } } // Each feature module gets its own scoped bucket client @Module({ imports: [StorageModule.forFeature('avatars')], }) export class UsersModule {} @Module({ imports: [StorageModule.forFeature('documents')], }) export class DocumentsModule {} ``` `StorageService` is registered globally once via `forRoot`. Each `forFeature` call creates a scoped provider that wraps the shared service with a specific bucket name. The underlying connection is not duplicated.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.