Dynamic Modules
Make a module configurable by its importer with a static register() that returns a DynamicModule, and import a module built by ConfigurableModuleBuilder with options and a global flag.
What you'll learn
- Write a static register() that returns a DynamicModule with the importer's options as a provider
- Import a dynamic module with options, and tell register(), forRoot() and forFeature() apart
- Read a module built with ConfigurableModuleBuilder, and use setExtras options such as isGlobal
Every module so far has been static: its providers, and the values they were built from, were fixed when the file was written. CatsModule knew it kept three cats because its own decorator said so. That is fine for a feature module that belongs to one application. It fails for a module meant to be reused, a logger, a database client, a configuration reader, because the importer is the one who knows the prefix, the connection string or the folder to read, and a static module gives it no way to say. A dynamic module is a module that is built when it is imported, from options the importer passes in.
The shape
A dynamic module is a static method on the module class that returns a DynamicModule: the same properties @Module() takes, plus module, the class itself:
@Module({})
export class ConfigModule {
static register(options: ConfigOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{ provide: CONFIG_OPTIONS, useValue: options },
ConfigService,
],
exports: [ConfigService],
};
}
}
The importer calls it inside imports:
@Module({
imports: [ConfigModule.register({ folder: './config' })],
})
export class AppModule {}
The trick is in the first provider. The options object becomes a provider under a token, useValue, and anything in the module can inject it. ConfigService asks for @Inject(CONFIG_OPTIONS) and reads folder from it; the module has been told how to behave without a line of it changing. The @Module({}) decorator on the class stays, empty, because the class is still a module; the metadata just arrives at import time instead.
Naming
The community agreed on names, and @nestjs packages follow them, so you will meet them everywhere:
register(): configure this import for its own use. Two importers may register the module with different options and get different instances.forRoot(): configure once, at the root, and reuse everywhere. Used for things there is one of: a database connection, the configuration.forFeature(): adjust aforRoot()module for one feature.TypeOrmModule.forFeature([Cat])is the classic: the connection came fromforRoot(), the entities from each feature.registerAsync(),forRootAsync(),forFeatureAsync(): the same, with the options built by a factory that can inject other providers, for options that come from configuration rather than a literal.
Letting Nest write it
Most dynamic modules are the pattern above with nothing special in it, so @nestjs/common ships ConfigurableModuleBuilder, which generates the class:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<LoggingOptions>().build();
@Module({
providers: [Logger],
exports: [Logger],
})
export class LoggingModule extends ConfigurableModuleClass {}
LoggingModule now has register(options) and registerAsync(...), and the options are provided under MODULE_OPTIONS_TOKEN for Logger to inject. setClassMethodName('forRoot') renames the methods. setExtras() adds options that configure the module itself rather than its providers: the usual one is isGlobal, mapped onto the module definition's global flag, so an importer can write LoggingModule.register({ prefix: 'app', isGlobal: true }) and every other module sees the logger without importing anything.
The docs put the builder call in its own file, logging.module-definition.ts, and so does this lesson. The reason is not taste. The module file imports the logger to provide it, and the logger imports the token to inject the options; if the token lived in the module file, each file would import the other, and the second one to load would read the token before it exists. Lesson 6 is about that error.
Your task
CatsModule becomes reusable: whoever imports it says how many cats it keeps. LoggingModule is already dynamic, built with ConfigurableModuleBuilder, and its logger prefixes every line with what it was registered with.
- Write
CatsModule.register(options). It returns the whole module: the controller, the options provided underCATS_CONFIG, the providers already listed in the file, and theCATS_STOREalias exported forStatsController. - In
AppModule, import the logging module with the prefixshelter, global soCatsModulecan inject the logger without importing anything, and the cats module configured for two cats.
GET /cats/config shows what the module was registered with, and GET /logs shows the prefix.
When it fails
- Every cats route is a 404:
register()returns a module without controllers, orAppModuledoes not import it. Nest can't resolve dependencies of the CatsRepository (?),Symbol(CATS_CONFIG): the options are not provided under the token inside the returned module.Nest can't resolve dependencies of the CatsService (CatsRepository, ?),Logger: the logging module is not global, soCatsModulecannot see its export. PassisGlobal: true.Nest can't resolve dependencies of the StatsController (?): the alias is not in the dynamic module'sexports.
Remember
- A dynamic module is a static method returning
{ module, ...metadata }, called inside the importer'simports. - The importer's options become a provider under a token, and the module's own classes inject it.
register()for per-import configuration,forRoot()for once,forFeature()for a feature's addition;...Asyncvariants build options from other providers.ConfigurableModuleBuilderwrites the pattern for you;setExtras()is for options about the module itself, such asisGlobal.
Stuck? Show a hint
register() returns { module: CatsModule, controllers: [CatsController], providers: [{ provide: CATS_CONFIG, useValue: options }, ...catsProviders], exports: ['CATS_STORE'] }. In AppModule: LoggingModule.register({ prefix: 'shelter', isGlobal: true }) and CatsModule.register({ maxCats: 2 }).