Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, NestJS 12.1.2, TypeScript 6.0.3 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A dynamic module is a module made by a method call · forRoot, register and forFeature tell callers how often to configure. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

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.

sms/src/sms.ts TypeScript · sms/src/sms.ts
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:

Interactive · step by step What Nest does with SmsModule.forRoot() Step through one app's import of SmsModule.forRoot(), then a second app's, then a plain import of the class.
What Nest does with SmsModule.forRoot(), step 6 of 6. Showing TiffinTrailApp (imports the class only), @Module() of SmsModule, SmsService. In focus: SmsService.providesTiffinTrailAppimports the class only@Module() of SmsModuleproviders: SmsServiceSmsServiceneeds SMS_OPTIONSSMS_OPTIONS missing

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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].

One SmsModule, three apps JavaScript · sms/forroot.mjs
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

  1. Question 1 of 2 An app imports the class SmsModule itself, not SmsModule.forRoot(). SmsModule's @Module() lists SmsService, which needs SMS_OPTIONS. What happens when the app starts?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Start-up stops, because SMS_OPTIONS is added only by forRoot(), so SmsService cannot be built

    The class carries only what @Module() lists. The settings, and global: true, exist only in the object that a call to forRoot() returns.

  2. Question 2 of 2 What does Nest do with the object that SmsModule.forRoot() returns?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It adds the object's providers, exports and global flag to what SmsModule's @Module() already lists

    A DynamicModule is the module class plus metadata; Nest merges that metadata with the class's own, so a module can keep its fixed providers in @Module() and take its settings from the call.

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

  1. Question 1 of 2 Two feature modules each need a payment client with their own base URL, and neither should see the other's. Which method name should the client module offer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: register(), so that each importer configures its own copy

    The name does not change what Nest does with the returned object; it tells callers how often to call it.

  2. Question 2 of 2 The SMS key lives in a secrets store that answers only when the app is starting. Why can SmsModule.forRoot({ apiKey }) in AppModule's imports not take it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: forRoot() runs when the module file is loaded, before any provider exists; forRootAsync() lets a factory build the options while the app starts

    The async variants take imports, inject and useFactory (or useClass, useExisting) instead of the options, and Nest awaits the factory while the app starts.

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.

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.

  1. Question 1 of 7 What does a static method such as SmsModule.forRoot() return?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: A DynamicModule, an object with the module class and the metadata to add to it

    The returned object goes into an imports array like a module class, and Nest reads it as more metadata for that class.

  2. Question 2 of 7 SmsModule's @Module() lists SmsService and exports it; forRoot() returns providers with SMS_OPTIONS only. Which providers does SmsModule have in an app that imports SmsModule.forRoot()?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: SmsService and SMS_OPTIONS

    That split is the usual design: fixed providers in @Module(), the caller's settings in the call.

  3. Question 3 of 7 SmsModule.forRoot() returns global: true. Without it, what would OrdersModule, whose OrdersNotifier injects SmsService, need in order to start?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: An import that brings it the configured SmsModule's exports, since a module that is not global reaches only its importers

    global: true is what lets a forRoot() module configured once in the root reach every feature; without it the usual rules of imports and exports apply.

  4. Question 4 of 7 Which method name tells a caller to configure a module once, in the root module?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: forRoot()

    The names are conventions, but callers read them as rules about how often to call each method.

  5. Question 5 of 7 A module offers forRoot() and register() with identical code. What does Nest do differently between them?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: Nothing; only the callers' expectations differ

    The name is documentation for callers: forRoot() once, register() per importer, forFeature() per feature.

  6. Question 6 of 7 A cache module needs the Redis address once for the whole app, and each feature declares its own key prefix. Which methods should it offer?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: forRoot() and forRootAsync() for the address, and forFeature() for each feature's prefix

    This is the TypeOrmModule shape: one root configuration, and forFeature() per feature for what is its own.

  7. Question 7 of 7 Without setClassMethodName(), which static methods does ConfigurableModuleBuilder generate?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: register() and registerAsync()

    Choose the name with setClassMethodName() to match how callers should use the module.

Interview questions

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

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, and forFeature() 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.