Creating Dynamic Module Nest JS Part-1

Creating Dynamic Modules in Nest JS Part-1

Code https://github.com/tkssharma/blogs/tree/master/nestjs-dynamic-module This is really a hot topic in nestjs and there is not much content available on dynamic Module.

Lets unfold the mystery of dynamic modules step by step What is nestjs Module, something which we are writing in every nestjs projects

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

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

Module is Just a collection of controllers, providers and exports and these modules can be shared and used by other Modules Lets say i have created sendGrid Module, AzureBlobModule or Database Module, These Module will be used by other Modules and sometime when we import these modules we also need to pass configuration like DatabaseModule will need DB connection Url, Azure Module may need Azure Connection details for Blob Upload

Most of the times we do static Module Import Like UserModule importing Account Module and we are Importing Both in Root Module we don't need to pass any configurations there

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

In this Example what if i want to configure UserModule based on the use case i have with Auth Module

Dynamic module use case

With static module binding, there's no opportunity for the consuming module to influence how providers from the host module are configured. Why does this matter? Consider the case where we have a general purpose module that needs to behave differently in different use cases. This is analogous to the concept of a "plugin" in many systems, where a generic facility requires some configuration before it can be used by a consumer.

et's consider what a dynamic module import, where we're passing in a configuration object, might look like. Compare the difference in the imports array between these two examples: ConfigModule from nestjs is a dynamic Module as i can pass my own configuration before using this my module

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

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • ConfigModule is a normal class, so we can infer that it must have a static method called register(). We know it's static because we're calling it on the ConfigModule class, not on an instance of the class. Note: this method, which we will create soon, can have any arbitrary name, but by convention we should call it either forRoot() or register().
  • The register() method is defined by us, so we can accept any input arguments we like. In this case, we're going to accept a simple options object with suitable properties, which is the typical case.

Lets have a look how it looks like

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [ConfigService],
      exports: [ConfigService],
    };
  }
}

import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor() {
    const options = { folder: './config' };

    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

In this above example const options = { folder: './config' }; we are not using passed value from Module, we need to find a way to Use the path passed in ConfigModule.register({ folder: './config' } method what we need to do is define our options object as a provider. This will make it injectable into the ConfigService, which we'll take advantage of in the next step. In the code below, pay attention to the providers array:

@Module({})
export class ConfigModule {
  static register(options): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}
// One final note: for simplicity we used a string-based injection token ('CONFIG_OPTIONS') above, but best practice is to define it as a constant (or Symbol) in a separate file, and import that file. For example:

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';

@Inject('CONFIG_OPTIONS') private options will be able to inject the options which contains the path of the config file

Examples of Existing Dynamic Modules

In this Example someonr already built the dynamic Module and we are using it by passing our option Object we are getting from Config Service

import { SendGridModule } from "@ntegral/nestjs-sendgrid";

@Global()
@Module({
  imports: [
    SendGridModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        apiKey: config.get("SENDGRID_ACCESS_KEY") || "",
      }),
    }),
  ],
  providers: [SendgridService],
  exports: [SendgridService],
})
export class SendgridModule {}

Here we can see these Modules which are already available as NPM Module exposing forRoot and forRootAsync methods to dynamically initialize these modules

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

// or  forRootAsync 
TypeOrmModule.forRootAsync({
  useFactory: async () =>
    Object.assign(await getConnectionOptions(), {
      autoLoadEntities: true,
    }),
});

Custom Dynamic Module on @nestjs/typeorm

We can create Database Module which will Use TypeORM Module and will access configurations from config Module

@Module({})
export class DatabaseModule {
  private static getConnectionOptions(config: ConfigService, dbconfig: DbConfig): TypeOrmModuleOptions {
    const dbdata = config.get().db;
    if (!dbdata) {
      throw new DbConfigError('Database config is missing');
    }
    const connectionOptions = DbModule.getConnectionOptionsPostgres(dbdata);
    return {
      ...connectionOptions,
      entities: dbconfig.entities,
      synchronize: false,
      logging: false,
    };
  }

  public static forRoot(dbconfig: DbConfig): DynamicModule {
    return {
      module: DbModule,
      imports: [
        TypeOrmModule.forRootAsync({
          imports: [ConfigModule, AppLoggerModule],
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          useFactory: (configService: ConfigService, logger: Logger) => DbModule.getConnectionOptions(configService, dbconfig),
          inject: [ConfigService],
        }),
      ],
      controllers: [],
      providers: [DatabaseService],
      exports: [DatabaseService],
    };
  }
}

Later in root module we can call root static Method to initialize this module asynchronously

@Module({
  imports: [
    DbModule.forRoot({
      entities: [Entity1, Entity2],
    })
  ]
});

Or we can also do it in same Module the whole idea is to pass the database configuration from config Module, config service

@Global()
@Module({
  imports: [
    SendGridModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
      entities: [Entity],
      synchronize: false,
      logging: config.get().db.logging,
      type: config.get().type
      url: config.get().db.url
      keepConnectionAlive: true,
      ssl: false
      }),
    }),
  ],
  providers: [],
  exports: [],
})
export class DatabaseModule {}

Conclusion

  • All these examples are talking about what is the use case of dynamic Module and how to use it, like existing library @nestjs/typeorm, sendGridModule and many more
  • In next part we can also create our own dynamic Module and use it in another Module, when i say our own module which will be same as @nestjs/typeorm, nestjs config Module exposing forRoot and forRootAsync methods to initialize module dynamically

Comments