Create dynamic module made easy in NestJS 9

Reading Time: 4 minutes

Loading

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.

let's go

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 export ConfigurableModuleClass and MODULE_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:

  1. https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder
  2. nestjs-world-cup-lib repo: https://github.com/railsstudent/nestjs-world-cup-lib
  3. Source code of ConfigurableModuleBuilder – https://github.com/nestjs/nest/blob/master/packages/common/module-utils/configurable-module.builder.ts
  4. How to update nestjs: https://blog.bitsrc.io/how-to-update-nest-js-ecd0e9466165