Creating Dynamic Modules in Nest JS Part-1

tkssharma

tkssharma

Posted on March 31, 2022

Creating Dynamic Modules in Nest JS Part-1

Creating Dynamic Modules in Nest JS Part-1

'dynamic module nestjs'
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.

Blog Originally Published here https://tkssharma.com/nestjs-creating-dynamic-modules-part-1/

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 {}


Enter fullscreen mode Exit fullscreen mode

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 {}


Enter fullscreen mode Exit fullscreen mode

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 {}


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



Enter fullscreen mode Exit fullscreen mode

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';



Enter fullscreen mode Exit fullscreen mode

@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 {}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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: DatabaseModule,
      imports: [
        TypeOrmModule.forRootAsync({
          imports: [ConfigModule, AppLoggerModule],
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          useFactory: (configService: ConfigService, logger: Logger) => DatabaseModule.getConnectionOptions(configService, dbconfig),
          inject: [ConfigService],
        }),
      ],
      controllers: [],
      providers: [DatabaseService],
      exports: [DatabaseService],
    };
  }
}


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode




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

References

💖 💪 🙅 🚩
tkssharma
tkssharma

Posted on March 31, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related