This chapter follows Configuration in NestJS.
The modules in Modules in NestJS are static: their providers are fixed in the @Module() decorator, and every importer gets the same thing. A reusable module, such as a database connection, a mail client, or ConfigModule itself, needs input from the app that uses it: a URI, an API key, a list of entities. A dynamic module is a static method on the module class that takes that input and returns module metadata built from it.
What a dynamic module returns
The method returns a DynamicModule: the same keys as @Module(), plus a module key naming the class. Options usually travel as a provider under a token of their own, so the module’s services can inject them:
import { DynamicModule, Module } from '@nestjs/common';import { MailService } from './mail.service';
export const MAIL_OPTIONS = Symbol('MAIL_OPTIONS');
export interface MailOptions { apiKey: string; from: string;}
@Module({})export class MailModule { static forRoot(options: MailOptions): DynamicModule { return { module: MailModule, providers: [{ provide: MAIL_OPTIONS, useValue: options }, MailService], exports: [MailService], }; }}@Injectable()export class MailService { constructor(@Inject(MAIL_OPTIONS) private readonly options: MailOptions) {}}The metadata returned by forRoot() extends whatever the @Module() decorator declares. Adding global: true to the returned object makes the module global, which is how ConfigModule.forRoot({ isGlobal: true }) works.
forRoot, forFeature, and register
The method names are a convention that tells the reader how a module is meant to be used:
| Method | Called | Typical meaning |
|---|---|---|
forRoot() |
Once, in the root module | Configure a shared resource, such as one connection, for the whole app |
forFeature() |
In each feature module | Use the shared resource with feature-specific settings, such as which models to register |
register() |
Wherever it is imported | Create a separate configured instance for this importer |
MongooseModule.forRoot() opens the connection once. MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]) in CatsModule registers the Cat model on that connection and makes it injectable in that module only. The Mongoose chapter uses both.
Async configuration
Options passed to forRoot() must exist when the decorator is evaluated, which is before Nest has created a single provider. When the options come from ConfigService, they are not available yet. forRootAsync() moves the work into the injector by accepting a factory instead of a value:
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), MailModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ apiKey: config.getOrThrow('MAIL_API_KEY'), from: config.get('MAIL_FROM', 'noreply@example.com'), }), }), ],})export class AppModule {}Inside the module, the factory simply becomes the options provider: { provide: MAIL_OPTIONS, useFactory: options.useFactory, inject: options.inject }. The factory may be async, for example to fetch a secret from a vault, and Nest waits for it before creating anything that depends on the options. An imports key lists modules the factory’s dependencies come from, when they are not global. Most library modules accept useClass and useExisting here as well.
Generating the boilerplate
Writing forRoot, forRootAsync, the options token, and their types by hand repeats across every reusable module. ConfigurableModuleBuilder generates them:
import { ConfigurableModuleBuilder } from '@nestjs/common';import type { MailOptions } from './mail.options';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new ConfigurableModuleBuilder<MailOptions>().setClassMethodName('forRoot').build();@Module({ providers: [MailService], exports: [MailService],})export class MailModule extends ConfigurableModuleClass {}MailModule now has forRoot() and forRootAsync() with the right types, and MailService injects its options with @Inject(MODULE_OPTIONS_TOKEN). setExtras() adds options that shape the module rather than the service, such as an isGlobal flag.
When to write one
Most feature modules in an application stay static. A dynamic module earns its place when the same module is used with different settings, or when it wraps a client library that needs configuration before it can be created: a mail provider, a payment SDK, a message broker. If a module is only ever imported once and reads its settings from ConfigService directly, a static module that injects ConfigService is simpler and just as testable.
Check your understanding
Question 1 of 5
0 of 5 checked