Introduction
Our technology team builds a lot of Nestjs libraries to reuse in Nestjs applications. Our libraries provide forRoot()
to create dynamic module only. Therefore, we cannot call forRootAsync()
to register asynchronous module by importing modules and injecting services.
Before V9, there are many boilerplate codes to get forRootAsync()
to work. Internally, forRootAsync
invokes createAsyncProvider
and createAsyncOptionsProviders
private static methods and both of them accept some sort of ModuleAsyncOptions interface. As the result, the team avoids the implementation unless it is absolutely required.
NestJS 9 introduces ConfiguratableModuleBuilder
and it simplifies the steps to create dynamic module by eliminating most of the boilerplate codes.
In this blog post, I describe how to create a nestjs library that provides forRoot and forRootAsync with ConfigurableModuleBuilder. Then, I can install it in a new nestjs 9 app to register the module either synchronously or asynchronously.
Upgrade NestJS to V9
npm install -g @nestjs/cli npm-check-updates
nest update
npm-check-updates "/@nestjs*/" -u
Create a new nestjs library
nest new nestjs-world-cup-lib
The example is a dummy library to return World Cup finals from 1930 to 2018.
Rename src/
to lib
/ and update tsconfig.json accordingly
nestjs-world-cup-lib
├── README.md
├── cspell.json
├── lib
│ ├── enums
│ │ ├── countries.enum.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── interfaces
│ │ ├── index.ts
│ │ ├── world-cup-module-options.interface.ts
│ │ └── world-cup-result.interface.ts
│ ├── services
│ │ ├── index.ts
│ │ └── world-cup.service.ts
│ ├── world-cup-module-definition.ts
│ └── world-cup.module.ts
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./lib",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"strict": true,
"strictPropertyInitialization": false,
"noImplicitThis": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"noUncheckedIndexedAccess": true
},
"include": ["lib/**/*"],
"exclude": ["dist", "test", "node_modules"]
}
Update outDir to ./dist
and rootDir to ./lib
. Include ['lib/**/*']
and excludes ["dist", "test", "node_modules"]
from compilation
Steps to create dynamic module
- Create an interface for the options
- Instantiate
ConfigurableModuleBuilder
and exportConfigurableModuleClass
andMODULE_OPTIONS_TOKEN
- Generate providers, services, etc that register and export from the new module
- Add the configurable module that extends
ConfigurableModuleClass
import { ConfigurableModuleBuilder } from '@nestjs/common'
import { WorldCupModuleOptions } from './interfaces'
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<WorldCupModuleOptions>()
.setClassMethodName('forRoot')
.build()
Step 1: Create an interface for the options
The simple interface has 2 options: the year World Cup is held and your favorite world cup team
// world-cup-module-options.interface.ts
export interface WorldCupModuleOptions {
year: number
favoriteCountry: string
}
Step 2: Instantiate ConfigurableModuleBuilder
This step is identical for every dynamic module. We instantiate ConfigurableModuleBuilder
and export ConfigurableModuleClass
and MODULE_OPTIONS_TOKEN
// world-cup-module-definition.ts
import { ConfigurableModuleBuilder } from '@nestjs/common'
import { WorldCupModuleOptions } from './interfaces'
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<WorldCupModuleOptions>()
.setClassMethodName('forRoot')
.build()
ConfigurableModuleBuilder generates register
and registerAsync
by default. In my example, I want to use forRoot
and forRootAsync
instead. Therefore, I override the configuration by setClassMethodName('forRoot')
.
Step 3: Generate a service and export from module
// world-cup.service.ts
@Injectable()
export class WorldCupService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: WorldCupModuleOptions) {}
getYear(): number {
return this.options.year
}
}
The service injects MODULE_OPTIONS_TOKEN
in the constructor to access options. We can access the values by this.options.year and this.options.favoriteCountry respectively.
Add the configurable module that extends ConfigurableModuleClass
import { Module } from '@nestjs/common'
import { WorldCupService } from './services'
import { ConfigurableModuleClass } from './world-cup-module-definition'
@Module({
providers: [WorldCupService],
exports: [WorldCupService],
})
export class WorldCupModule extends ConfigurableModuleClass {}
The new module extends ConfigurableModuleClass
but we can register and export services the same way as before.
The implementation of the library is done, we can publish it to npmjs and try it in a new nestjs V9 application
Register dynamic module and test in nestjs V9 application
npm i nestjs-world-cup-lib
import { WorldCupModule } from 'nestjs-world-cup-lib'
@Module({
imports: [
/* Register synchronous dynamic module */
WorldCupModule.forRoot({
year: 2018,
favoriteCountry: 'Brazil'
})
/* Register asynchronous dynamic module
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
YEAR: Joi.number().optional(),
FAVORITE_COUNTRY: Joi.string().optional(),
}),
}),
WorldCupModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (service: ConfigService) => ({
year: service.get<number>('YEAR'),
favoriteCountry: service.get<string>('FAVORITE_COUNTRY'),
})
})
*/
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Test WorldCupService
in AppController
import { WorldCupService } from 'nestjs-world-cup-lib'
@Controller()
export class AppController {
constructor(private worldCupService: WorldCupService) {}
@Get('world-cup-year')
getWorldCupYear(): number {
return this.worldCupService.getYear()
}
}
Type http://localhost:3000/world-cup-year
in the browser to verify the endpoint.
Final Thoughts
In this post, we see one of the new changes of nestjs v9 that is ConfigurableModuleBuilder. ConfigurableModuleBuilder class simplify the process to create dynamic module. When dynamic module can register without dependency, register
, forRoot
and forFeature
methods are suffice. Alternatively, dynamic module that depends on dependency to initialize configurable options should use registerAsync
, forRootAsync
and forFeatureAsync
counterparts.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.
Resources:
- https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder
- nestjs-world-cup-lib repo: https://github.com/railsstudent/nestjs-world-cup-lib
- Source code of ConfigurableModuleBuilder – https://github.com/nestjs/nest/blob/master/packages/common/module-utils/configurable-module.builder.ts
- How to update nestjs: https://blog.bitsrc.io/how-to-update-nest-js-ecd0e9466165