DEV Community

Cover image for Best Way to Create Dynamic Modules in NestJS
mkadirtan for Noop Today

Posted on • Edited on • Originally published at nooptoday.com

Best Way to Create Dynamic Modules in NestJS

Creating dynamic modules in NestJS can be complex. Use this method and you will never be afraid of creating dynamic modules again!

Cover Photo by Westwind Air Service on Unsplash


NestJS module system is very helpful. They allow us to organize our code, and they help us 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 {}
Enter fullscreen mode Exit fullscreen mode

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

But things get complicated when you want to configure your module dynamically. This is typically required by database modules but there are many other cases that require you to create dynamic module 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 least complexity!


NestJS Official Docs are Too Complex

Here is a dynamic module example from the docs:

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import {
  ConfigurableModuleClass,
  ASYNC_OPTIONS_TYPE,
  OPTIONS_TYPE,
} from './config.module-definition';

@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {
  static register(options: typeof OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.register(options),
    };
  }

  static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
    return {
      // your custom logic here
      ...super.registerAsync(options),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

https://docs.nestjs.com/fundamentals/dynamic-modules#extending-auto-generated-methods

I know I've picked the most bloated example but still, creating a dynamic module as documented in NestJS docs, doesn't look simple. There is a good reason for that. On one hand NestJS tries to provide a simple API for using module system, but on the other hand the API must provide enough access to satisfy custom needs of developers. I think they did a great job at designing their API, but this is the reason examples from official docs are complex.

@golevelup Team To The Rescue

@golevelup team has some great packages to use along your NestJS project. We will use @golevelup/nestjs-modules for this post. We will use defaults provided by their package, in exchange our dynamic modules will be much simpler and easier to maintain.

I assume you've already spin up a NestJS project. We need to install @golevelup/nestjs-modules package to our project:

npm install @golevelup/nestjs-modules
Enter fullscreen mode Exit fullscreen mode

What Will We Build

For the sake of providing a good use case, lets create a serializer module that can use alternative strategies. Create the following files:

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

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 Configurable Dynamic Module

Create your module as usual:

// serde.module.ts
@Module({})
export class SerdeModule {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

One last thing, we will expose SerdeModule's functionality from SerdeService. Therefore it needs to be exported:

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 {}
Enter fullscreen mode Exit fullscreen mode

So far so good, and there is nothing unusual. Now we will make SerdeModule configurable, specifically we want user 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';
}
Enter fullscreen mode Exit fullscreen mode

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 is defined as follows:

declare type InjectionToken = string | symbol | Type;

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

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';
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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' })
    ]
})
Enter fullscreen mode Exit fullscreen mode

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')
                }
            }
        })
    ]
})
Enter fullscreen mode Exit fullscreen mode

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 });
    }
}

// console output: { moduleOptions: { strategy: 'json' } }
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (4)

Collapse
 
kostyatretyak profile image
Костя Третяк

As far as I understand, the main difficulty with NestJS dynamic modules is with asynchronous providers. Can someone explain to me why we need asynchronous providers at all if, for example, we can easily get an asynchronous connection to the database with common providers.

Collapse
 
mkadirtan profile image
mkadirtan

You can get asynchronous connection but the problem is if your configuration is loaded asynchronously, you must wait for it. For example, you might store some configuration options in your database. To provide configuration to other modules, first your Database module needs to be initialized, and fetch configuration. Other modules has to wait in such scenarios ( loading a file from disk, database query, http call etc. )

Collapse
 
kostyatretyak profile image
Костя Третяк • Edited

Why can't you create a service that goes to the database and asynchronously issues the configuration to all the modules that need it? This can be done by a regular provider (not asynchronous).

Thread Thread
 
mkadirtan profile image
mkadirtan • Edited

That's a great question and thanks for pointing out issues with async providers btw.

That is absolutely possible, and I've actually used your technique in some cases.

I think main reason NestJS has the concept of asynchronous providers, is to ensure a conventional way to handle such cases. It is opiniated about the way you structure your code, so that makes sense.