Photo by Yosuke Ota on Unsplash

On this page

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:

src/mail/mail.module.ts
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],
};
}
}
src/mail/mail.service.ts
@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:

src/app.module.ts
@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:

src/mail/mail.module-definition.ts
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();
src/mail/mail.module.ts
@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.

Next: Application lifecycle hooks in NestJS.

Check your understanding

Question 1 of 5

0 of 5 checked

  1. 1. How do options passed to forRoot() usually reach the module's services?

Search articles

Type a word to search published articles.

Open the search page