NestJS Module 4 – Modules, dynamic modules and lifecycle
Dynamic modules and ConfigurableModuleBuilder
Write forRoot, register and forFeature for a reusable Nest module, generate them with ConfigurableModuleBuilder, and read and check options at start-up.
What you will learn
- Write a dynamic module whose forRoot() returns the module's metadata with the caller's options
- Choose between forRoot, register and forFeature, and their async variants, for a module's callers
- Generate forRoot() and forRootAsync() with ConfigurableModuleBuilder, its options token and an isGlobal extra
- Configure a module from ConfigService with forRootAsync(), and validate its options while the app starts
- Predict how many instances of a provider an app gets when forRoot() is called once or in each feature
Before you start
What you’ll learn · Before you start
What you will learn
- Write a dynamic module whose forRoot() returns the module's metadata with the caller's options
- Choose between forRoot, register and forFeature, and their async variants, for a module's callers
- Generate forRoot() and forRootAsync() with ConfigurableModuleBuilder, its options token and an isGlobal extra
- Configure a module from ConfigService with forRootAsync(), and validate its options while the app starts
- Predict how many instances of a provider an app gets when forRoot() is called once or in each feature
Before you start
On this page
TiffinTrail sends lunch reminders by SMS, and so does Kharcha, a second app built on the same code: one SmsModule,
two sender ids, TFNTRL and KHRCHA, and two keys. A module described only by its @Module() decorator has nowhere to
take them.
dynamic module: A module configured by a method call, such as SmsModule.forRoot(options): the method returns the module class with extra metadata, often the caller's settings as a provider, which Nest adds to the class's @Module() lists. do: a static method takes the caller’s settings and returns the
module’s metadata with them. This lesson writes one by hand, names the conventions forRoot(), register() and
forFeature(), generates the methods with ConfigurableModuleBuilder, and reads the settings from ConfigService and
checks them while the app starts.
A dynamic module is a module made by a method call
SmsModule lists SmsService in its @Module() decorator, and SmsService asks for its settings under a symbol
token, SMS_OPTIONS, as the custom providers lesson did for
its gateway. The decorator cannot hold the settings, because they differ for every app. So the module gets a static
method, forRoot(options), that returns a DynamicModule: an object with the module class and the providers to add,
here the options as a useValue provider.
import { type DynamicModule, Inject, Injectable, Module } from '@nestjs/common';
export interface SmsOptions {
/** The sender name a phone shows, such as TFNTRL. */
senderId: string;
/** The gateway's key. The examples send through a local fake gateway, so any text works. */
apiKey: string;
}
export const SMS_OPTIONS = Symbol('SMS_OPTIONS');
export const SMS_TEMPLATES = Symbol('SMS_TEMPLATES');
@Injectable()
export class SmsService {
static built = 0;
constructor(@Inject(SMS_OPTIONS) private readonly options: SmsOptions) {
SmsService.built += 1;
}
/** Sends nothing: returns the line the local fake gateway would deliver. */
send(to: string, text: string): string {
return `${this.options.senderId} -> ${to}: ${text}`;
}
}
/** One feature's message templates, sent through the app's SmsService. */
@Injectable()
export class SmsTemplates {
constructor(
private readonly sms: SmsService,
@Inject(SMS_TEMPLATES) private readonly templates: Record<string, string>,
) {}
send(to: string, name: string): string {
return this.sms.send(to, this.templates[name] ?? `no template called ${name}`);
}
}
@Module({})
export class SmsFeatureModule {}
@Module({
providers: [SmsService],
exports: [SmsService],
})
export class SmsModule {
/** Configures the gateway once, in the root module; global, so that every feature can inject SmsService. */
static forRoot(options: SmsOptions): DynamicModule {
return {
module: SmsModule,
global: true,
providers: [{ provide: SMS_OPTIONS, useValue: options }],
};
}
/** Gives the calling feature its own templates, sent through the SmsService that forRoot() configured. */
static forFeature(templates: Record<string, string>): DynamicModule {
return {
module: SmsFeatureModule,
providers: [{ provide: SMS_TEMPLATES, useValue: templates }, SmsTemplates],
exports: [SmsTemplates],
};
}
} Nest reads the returned object as more metadata for SmsModule. The Nest documentation says it extends, rather than
overrides, what @Module() lists. Step through what happens in each app:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The app imports a call. TiffinTrailApp does not import the class SmsModule. It imports what SmsModule.forRoot() returned when the file was loaded, a plain object.
- The object is metadata. The object names the module, SmsModule, and the providers to add, here one provider, SMS_OPTIONS, whose useValue holds the sender id TFNTRL and the key. It also says global true, so features can inject SmsService without an import.
- Added to the @Module() list. Nest adds that metadata to what the @Module() decorator of SmsModule already lists, SmsService and its export. Dynamic metadata extends the decorator's lists; it never replaces them.
- SmsService gets the options. Inside SmsModule, SmsService asks for SMS_OPTIONS, and the provider forRoot() added fills it. The run printed TFNTRL -> cus-41 for TiffinTrailApp.
- Another app, another call. KharchaApp calls forRoot() with KHRCHA. That is a second object with its own SMS_OPTIONS, so its SmsService printed KHRCHA, from the same module code.
- A plain import has no options. An app that imports the class SmsModule gets only the @Module() part. SmsService is there and SMS_OPTIONS is not, so start-up stopped with Nest can't resolve dependencies of the SmsService, naming Symbol(SMS_OPTIONS) at index [0].
import { session } from '../session.mjs';
// One SmsModule, imported as a plain class and then configured by two apps with forRoot().
session(`
npx tsc -p tsconfig.json
node dist/forroot.js
`); Output
$ npx tsc -p tsconfig.json
$ node dist/forroot.js
SmsModule.forRoot() returned:
module: SmsModule, global: true
providers: { provide: Symbol(SMS_OPTIONS), useValue: { senderId: 'TFNTRL', apiKey: '…' } }
AppWithThePlainModule: Nest can't resolve dependencies of the SmsService (?). Please make sure that the argument Symbol(SMS_OPTIONS) at index [0] is available in the SmsModule module.
TiffinTrailApp: TFNTRL -> cus-41: Your lunch leaves the kitchen at 12:10
KharchaApp: KHRCHA -> cus-41: Your lunch leaves the kitchen at 12:10
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node forroot.mjs
The first lines show the object forRoot() returned: SmsModule, global: true and one provider holding the
settings. The plain import failed, because SmsService was there and its options were not. TiffinTrailApp and
KharchaApp each got an SmsService with its own sender, from one module’s code. Dynamic modules are how Nest’s own
packages take their settings, from database connections to GraphQL servers.
This forRoot() is meant to be called once, in the root module, which is why it returns global: true: every feature
can then inject SmsService without an import. That is the case in which the
modules lesson accepts a global module, infrastructure configured once for
the whole app. The forFeature() method at the bottom of the file belongs to the next section.
Key idea
A dynamic module is the module class plus the metadata a method call returns, so each importer can pass its own
settings, and Nest adds that metadata to the lists of the class’s @Module().
Checkpoint
Check: What a dynamic module adds to a module
forRoot, register and forFeature tell callers how often to configure
The method’s name is a convention, not a rule: the documentation says there is no hard rule, and Nest runs
forRoot() and register() alike. Nest’s own packages give the names fixed meanings, and callers rely on them:
| Method | Who calls it | What it configures | In Nest’s packages |
|---|---|---|---|
forRoot() |
the root module, once | one configuration for the whole app | TypeOrmModule.forRoot(), GraphQLModule.forRoot() |
register() |
each module that imports it | a configuration for that importer alone | an HTTP client with a base URL per feature |
forFeature() |
each feature, after forRoot() |
the feature’s own pieces, on top of the root’s settings | TypeOrmModule.forFeature(), a feature’s repositories |
forRootAsync(), registerAsync() |
as above | the same, from a factory that can inject other providers | settings read from ConfigService |
In SmsModule, forFeature() gives each feature its own message templates, which send through the one SmsService
that forRoot() configured. Each async variant takes imports, inject and useFactory instead of the options, so
the settings can come from ConfigService or a secrets store while the app starts, not when the file is loaded.
Instead of useFactory, a caller may pass useClass, a class with a method that returns the options (create() in
the modules ConfigurableModuleBuilder generates), or useExisting, a provider the app already has with that method.
Writing forRoot() and forRootAsync() by hand for every reusable module repeats the same code, and the async half
is the easy one to get wrong. ConfigurableModuleBuilder: A class of @nestjs/common that generates a dynamic module's methods, register() and registerAsync() unless renamed, from an options interface, together with the token its options are provided under., from
@nestjs/common, generates both from an options interface: its build() returns a class to extend and
MODULE_OPTIONS_TOKEN: The injection token under which a module built with ConfigurableModuleBuilder provides the options its caller passed; the module's classes inject the options with @Inject(MODULE_OPTIONS_TOKEN)., the token the options are provided under.
ConfigurableModuleBuilder names the methods register() and registerAsync() unless setClassMethodName() renames
them.
Name the method for its callers
Pick forRoot() when the app configures the module once, register() when each importer passes its own settings,
and forFeature() for a feature’s pieces of a module configured once. The code can be the same; the name tells a
caller how often to call it.
Checkpoint
Check: forRoot, register, forFeature and their async forms
ConfigurableModuleBuilder, generated and run, with forRootAsync() three ways Pro: opens with a Pro, Premium or Ultimate pass
ConfigurableModuleBuilder turns an options interface into forRoot(), forRootAsync() and an options token. This part
builds SmsModule with it, prints what it generated, and starts the app with each form of forRootAsync().
In this part:
- What build() returns, and the module built on it
- forRootAsync() three ways, and what start-up waits for
1 diagram · 4 runnable examples (JavaScript, TypeScript, TypeScript (sms.module-definition.ts), TypeScript (sms.module.ts)) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Options that stop start-up with a clear message, not on the first SMS Pro: opens with a Pro, Premium or Ultimate pass
A wrong sender id should stop a deploy, not the first lunch reminder of the day. This part adds one check that covers all three ways the options arrive, starts the app with two bad configurations, and has you write the same check for a rate card.
In this part:
- One check for every way the options arrive, run against two bad deploys
- What an options check covers, with a rate card to practise on
- Check a module's options once, while the app starts
- Results of the sample tests
2 runnable examples (JavaScript, TypeScript) · 1 exercise with sample tests · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
How many instances forRoot() gives you, measured in six apps Pro: opens with a Pro, Premium or Ultimate pass
Whether two features share one SmsService depends on how often forRoot() runs and on how Nest tells modules apart.
This part counts the instances in six apps and shows where forFeature() fits.
In this part:
- forRoot() in each feature, counted in four apps
- forFeature() on top of one forRoot(), and without it
1 runnable example (JavaScript) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: dynamic modules
The questions check the two ideas of the first sections: what a forRoot() call adds to a module, and which convention
fits a module’s callers.
Practice that adapts to you
Practise this lesson
7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
What is the difference between forRoot() and register() in a Nest dynamic module, and does Nest treat them differently?
Nest treats them the same: both are static methods that return a DynamicModule, the module class plus metadata such as providers with the caller's settings, and Nest adds that metadata to the class's @Module() lists. The difference is a convention that Nest's own packages follow and callers rely on. forRoot() configures a module once, in the root module, for the whole app, as TypeOrmModule.forRoot() does. register() configures it for one importer, so each module that imports it can pass its own settings. forFeature() then adds a feature's own pieces on top of one forRoot(), and the async variants, forRootAsync() and registerAsync(), build the options with a factory that can inject providers such as ConfigService. ConfigurableModuleBuilder generates register() by default, and setClassMethodName('forRoot') renames it.
-
Mid to senior
Three internal apps will share an SmsModule. Each passes a sender id and an API key; one app reads them from a secrets store at start-up, and a wrong sender id must stop the deploy rather than fail the first SMS. Describe the module's API, how you would build it, and where each check runs.
-
Mid to senior
After a refactor, each instance of an app opens 20 database connections instead of 10. You find DatabaseModule.forRoot() with the same settings in OrdersModule and in BillingModule. Explain why there are two pools, and how you would fix it so that it stays fixed.
-
Mid to senior
SmsModule, built with ConfigurableModuleBuilder, is configured with forRootAsync({ inject: [ConfigService], useFactory }). Start-up stops: Nest can't resolve dependencies of the SMS_MODULE_OPTIONS (?), ConfigService at index [0]. ConfigModule.forRoot() is in AppModule's imports. Why, and what are two ways to fix it?
Key takeaways
- A dynamic module is a module class plus the metadata that a static method returns with the caller’s settings, and
Nest adds that metadata to the lists of the class’s
@Module(). - Importing the class alone gives only its
@Module()part, so a provider that needs the options cannot be built. forRoot()configures a module once for the app,register()once for each importer, andforFeature()adds a feature’s own pieces; the async variants build the options with a factory while the app starts.- ConfigurableModuleBuilder generates those methods and the options token from an options interface.
Terms in this lesson
- ConfigurableModuleBuilder
- A class of @nestjs/common that generates a dynamic module's methods, register() and registerAsync() unless renamed, from an options interface, together with the token its options are provided under.
- dynamic module
- A module configured by a method call, such as SmsModule.forRoot(options): the method returns the module class with extra metadata, often the caller's settings as a provider, which Nest adds to the class's @Module() lists.
- MODULE_OPTIONS_TOKEN
- The injection token under which a module built with ConfigurableModuleBuilder provides the options its caller passed; the module's classes inject the options with @Inject(MODULE_OPTIONS_TOKEN).
References
- NestJS documentation: Dynamic modules (conventions and ConfigurableModuleBuilder) (NestJS)
- NestJS documentation: Modules (dynamic modules) (NestJS)
- NestJS documentation: Configuration (ConfigModule and ConfigService) (NestJS)
- @nestjs/common source: module-utils/configurable-module.builder.ts (NestJS (GitHub))
- @nestjs/common source: interfaces/nest-application-context-options.interface.ts (moduleIdGeneratorAlgorithm, abortOnError) (NestJS (GitHub))
- @nestjs/core source: errors/exceptions-zone.ts (what a start-up error does) (NestJS (GitHub))
Report a problem with this lesson
Kept only in this browser. Your Learn progress