Best Way to Create Dynamic Modules in NestJS [ Updated Guide 2024 ]

The module system in NestJS is well-designed. It helps organize the code, and define module boundaries. Within @Module you can define everything about your module. From the NestJS docs:

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { CommonModule } from '../common/common.module';

@Global()
@Module({
  imports: [CommonModule],
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

https://docs.nestjs.com/modules#global-modules

But things get complicated when you want to configure a module dynamically. This is typically required by database modules but many other cases require you to create dynamic modules in NestJS.

As a convention dynamic modules in NestJS have static methods such as forRoot, forRootAsync, forFeature In this post you will learn how to create a dynamic module in NestJS with all the benefits but with the least complexity!

Method 1: NestJS Native

Creating dynamic modules in NestJS is also relatively easy with native helpers. NestJS provides an important helper to help create dynamic modules, namely ConfigurableModuleBuilder from the @nestjs/common package.

The official docs also mention this technique however, it is more informative about the inner workings rather than a practical guide. This is a step by step guide to create dynamic modules in NestJS.

Serde - Dynamic Module

For the sake of providing a good use case, let's create a serializer module that can use alternative strategies to serialize / deserialize objects depending on the configuration.

First, create a new project:

nest new dynamic-module-example

Then create the following resources:

# first cd into the directory
cd dynamic-module-example
nest generate module serde --no-spec
nest generate provider serde/msgpack.provider --flat --no-spec
nest generate provider serde/json.provider --flat --no-spec

copy and paste to generate resources

JSON is a more readable format, while MsgPack can be more efficient depending on the use case. In any case, we might want to change a configuration value to switch between strategies and the rest of the program should work fine.

To make sure both MsgpackProvider and the JsonProvider share the same interface, we should also create a common interface.

nest generate interface serde

We can define our interface as follows:

// serde.interface.ts

export interface SerdeInterface {
  parse(data)
  serialize(data)
}

Now, we can start working on our dynamic module. In the end, we want our module to be used as follows:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SerdeModule } from './serde/serde.module';

@Module({
  imports: [SerdeModule.register({ strategy: 'json' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Creating Dynamic Module Definition

So, we need to define our module options. Create one last file in the serde directory:

touch ./src/serde/serde.module-definition.ts

The final folder structure should look something like this:

./src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
└── serde
    ├── json.provider.ts
    ├── msgpack.provider.ts
    ├── serde.interface.ts
    ├── serde.module-definition.ts
    └── serde.module.ts

Now, we can use NestJS helpers to create our module options.

// serde.module-definition.ts

import { ConfigurableModuleBuilder } from "@nestjs/common";

type SerdeModuleOptions = {
    strategy: 'json' | 'msgpack'
}

const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new ConfigurableModuleBuilder<SerdeModuleOptions>().build();

export const Serializer = Symbol("serializer");

export {
    ConfigurableModuleClass as SerdeConfigurableClass,
    MODULE_OPTIONS_TOKEN as SERDE_OPTIONS_TOKEN,
}

export type SerdeOptions = typeof OPTIONS_TYPE;
export type AsyncSerdeOptions = typeof ASYNC_OPTIONS_TYPE;

To explain what's going on in this code, you should focus on the ConfigurableModuleBuilder. It is a builder class that generates the required tokens and class for us. We will use the ConfigurableModuleClass to extend our SerdeModule.

Notice that I renamed the output of the builder. It is not a necessary step but prevents confusion with other modules. It is always good practice to assign meaningful names.

And what is Serializer? It is the injection token we will use to inject select provider.

Extending Regular Module Into Dynamic Module

At the moment, the serde.module.ts look like this:

import { Module } from '@nestjs/common';

@Module({})
export class SerdeModule {}

Now we should use SerdeConfigurableClass to extend our regular module into a dynamic module.

import { Module } from '@nestjs/common';
import { SerdeConfigurableClass } from './serde.module-definition';

@Module({})
export class SerdeModule extends SerdeConfigurableClass {}

Without any further steps, the dynamic module is ready. It is pretty dull at the moment, but if you try to import it in the AppModule you can see it supports the register, registerAsync methods automatically.

To spice things up, we can add our custom logic now.

import { DynamicModule, Provider } from '@nestjs/common';
import { MsgpackProvider } from './msgpack.provider';
import { JsonProvider } from './json.provider';
import { SerdeConfigurableClass, SerdeOptions, Serializer } from './serde.module-definition';

export class SerdeModule extends SerdeConfigurableClass {

  static register(options: SerdeOptions): DynamicModule {
    const moduleDefinition = super.register(options);
    moduleDefinition.providers = moduleDefinition.providers || [];
    moduleDefinition.exports = moduleDefinition.exports || [];

    const serdeProvider: Provider = {
      provide: Serializer,
      useClass: options.strategy === 'json' ? JsonProvider : MsgpackProvider,
    }

    moduleDefinition.providers.push(serdeProvider);
    moduleDefinition.exports.push(serdeProvider);

    return moduleDefinition;
  }

}

What this does is, depending on the selected strategy, it chooses which provider to use for Serializer. Instead of hardcoding the provider in providers array, this achieves the same thing but in a configurable way.

Don't forget to implement the providers, here is my example implementation.

// json.provider.ts

import { Injectable } from '@nestjs/common';
import { SerdeInterface } from './serde.interface';

@Injectable()
export class JsonProvider implements SerdeInterface {
  serialize(data) {
    return JSON.parse(data)
  }

  parse(data) {
    return JSON.parse(data)
  }
}

and

// msgpack.provider.ts

import { Injectable } from '@nestjs/common';
import { SerdeInterface } from './serde.interface';
import { encode, decode } from "@msgpack/msgpack";

@Injectable()
export class MsgpackProvider implements SerdeInterface {
  serialize(data) {
    return encode(data);
  }

  parse(data) {
    return decode(data);
  }
}

If you've followed all the steps correctly, congratulations! You have created a dynamic module in NestJS. How to use it? Register it inside the AppModule.

Using The Dynamic Module

// app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SerdeModule } from './serde/serde.module';

@Module({
  imports: [SerdeModule.register({ strategy: 'json' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
}

Then, inject your provider within the AppService.

// app.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { Serializer } from "./serde/serde.module-definition";
import { SerdeInterface } from "./serde/serde.interface";

@Injectable()
export class AppService {
  constructor(@Inject(Serializer) private readonly serializer: SerdeInterface) {
  }

  getHello(): string {
    return this.serializer.serialize({ hello: 'world' })
  }
}

You can play around by changing the strategy and inspecting the results.

Method 2: Using @golevelup

The @golevelup team has some great packages to use along your NestJS project. We will use @golevelup/nestjs-modules to create dynamic modules in NesJS. We will use helpers from the package.

I assume you've already spun up a NestJS project. Now you should install the package.

npm install @golevelup/nestjs-modules

What Will We Build

We will build the same application in a different way, just so we can explore other possible techniques for creating dynamic modules. Create the following files:

# Inside src folder
/-> serde
/--> serde.module.ts
/--> serde.service.ts
/--> json.provider.ts
/--> msgpack.provider.ts
/--> constants.ts
/--> interfaces.ts

This is how we will continue:

  • Create a configurable dynamic module SerdeModule
  • Inject JsonProvider and MsgpackProvider to SerdeService
  • Define SerdeModuleOptions
  • Expose selected strategy from SerdeService

Creating a Configurable Dynamic Module

Create your module as usual:

// serde.module.ts
@Module({})
export class SerdeModule {}

Add providers to your module:

import { JsonProvider } from './json.provider';
import { MsgpackProvider } from './msgpack.provider';
import { SerdeService } from './serde.service';

@Module({
  providers: [
    JsonProvider,
    MsgpackProvider,
    SerdeService,
  ]
})
export class SerdeModule {}

One last thing, we will expose the SerdeService to be accessible from other modules. So it needs to be exported from SerdeModule:

import { JsonProvider } from './json.provider';
import { MsgpackProvider } from './msgpack.provider';
import { SerdeService } from './serde.service';

@Module({
  providers: [
    JsonProvider,
    MsgpackProvider,
    SerdeService,
  ],
  exports: [SerdeService]
})
export class SerdeModule {}

So far so good, and there is nothing unusual. Now we will make SerdeModule configurable, specifically we want users to decide which serialization strategy they want to use.

We need to create an interface to define module options:

// interfaces.ts
export interface SerdeModuleOptions {
  strategy: 'json' | 'msgpack';
}

Lastly, we will need these options to be accessible within SerdeService. If you remember from the NestJS docs, module options are injected within providers. And @Inject decorator provides values via an InjectionToken. Injection Token acts like a unique id of the value you want to access. It's type is defined in the NestJS core as follows:

declare type InjectionToken = string | symbol | Type;

export interface Type<T = any> extends Function {
    new (...args: any[]): T;
}

Type is basically a class reference. Since we defined our module options as an interface, we will define a string as Injection Token:

// constants.ts
export const SERDE_MODULE_OPTIONS_TOKEN = 'SERDE_MODULE_OPTIONS_TOKEN';

Finally we can bring them all together:

import { JsonProvider } from './json.provider';
import { MsgpackProvider } from './msgpack.provider';
import { SerdeService } from './serde.service';

import { createConfigurableDynamicRootModule } from '@golevelup/nestjs-modules';
import { SERDE_MODULE_OPTIONS_TOKEN } from './constants';
import { SerdeModuleOptions } from './interfaces';

@Module({
  providers: [
    JsonProvider,
    MsgpackProvider,
    SerdeService,
  ],
  exports: [SerdeService]
})
export class SerdeModule extends createConfigurableDynamicRootModule<SerdeModule, SerdeModuleOptions>(SERDE_MODULE_OPTIONS_TOKEN) {}

That is all! We've successfully created a dynamic module. Lets put it into use, we can import it within AppModule:

// app.module.ts
import { SerdeModule } from './serde.module';

@Module({
  imports: [
    SerdeModule.forRoot(SerdeModule, { strategy: 'json' })
  ]
})

Notice we've never defined a forRoot method on our class. This method is automatically created by @golevelup/nestjs-modules and it is type-safe, Hurray! There is also the forRootAsync counterpart:

// app.module.ts
import { SerdeModule } from './serde.module';
import { ConfigModule, ConfigService } from '@nestjs/config';


@Module({
  imports: [
    SerdeModule.forRootAsync(SerdeModule, {
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return {
          strategy: configService.get('SERDE_STRATEGY')
        }
      }
    })
  ]
})

Accessing Module Options

To make use of provided options, we need to inject it into SerdeService

import { SERDE_MODULE_OPTIONS_TOKEN } from './constants';
import { SerdeModuleOptions } from './interfaces';

@Injectable()
export class SerdeService {
  constructor(
    @Inject(SERDE_MODULE_OPTIONS_TOKEN)
    moduleOptions: SerdeModuleOptions
  ){
    console.log({ moduleOptions }); 
    // { moduleOptions: { strategy: 'json' } }
  }
}

We will apply strategy pattern for this example. If you want to learn more about strategy pattern, I recommend you to read https://betterprogramming.pub/design-patterns-using-the-strategy-pattern-in-javascript-3c12af58fd8a written by Carlos Caballero. Basically we will switch between JsonProvider and MsgpackProvider depending on the moduleOptions.strategy.

First inject the providers into SerdeService

import { SERDE_MODULE_OPTIONS_TOKEN } from './constants';
import { SerdeModuleOptions } from './interfaces';

import { JsonProvider } from './json.provider';
import { MsgpackProvider } from './msgpack.provider';

@Injectable()
export class SerdeService {
  private readonly _strategy;

  constructor(
    @Inject(SERDE_MODULE_OPTIONS_TOKEN)
    moduleOptions: SerdeModuleOptions,
    private readonly jsonProvider: JsonProvider,
    private readonly msgpackProvider: MsgpackProvider,
  ){
    switch(moduleOptions.strategy) {
      case 'json':
        this._strategy = jsonProvider;
        break;
      case 'msgpack':
        this._strategy = msgpackProvider;
        break;
    }
  }
}

Now, we can expose our API to outside world:

import { SERDE_MODULE_OPTIONS_TOKEN } from './constants';
import { SerdeModuleOptions } from './interfaces';

import { JsonProvider } from './json.provider';
import { MsgpackProvider } from './msgpack.provider';

@Injectable()
export class SerdeService {
  private readonly _strategy;

  constructor(
    @Inject(SERDE_MODULE_OPTIONS_TOKEN)
    moduleOptions: SerdeModuleOptions,
    private readonly jsonProvider: JsonProvider,
    private readonly msgpackProvider: MsgpackProvider,
  ){
    switch(moduleOptions.strategy) {
      case 'json':
        this._strategy = jsonProvider;
        break;
      case 'msgpack':
        this._strategy = msgpackProvider;
        break;
    }
  }
  
  public serialize(data) {
    return this._strategy.serialize(data);
  }
    
  public parse(data) {
    return this._strategy.parse(data);
  }
}
I leave implementing actual providers to you. If you want to learn more about Strategy Pattern, I am planning to write a detailed post about it. Tell me in the comments if you are interested!

We created a dynamic module in NestJS with an example use case. I hope you've learned something new, let me know what you think in the comments!