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.
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 {}
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: 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],
};
}
}
Later in root module we can call root static Method to initialize this module asynchronously
@Module({
imports: [
DatabaseModule.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