Dynamic modules in NestJS

NestJS is a framework that incorporates the benefits of TypeScript, IoC / DI and the Angular framework, and is rapidly evolving and gaining popularity.





Many techniques and practices are described in the official documentation . Consider writing your own Dynamic module and publishing it to npm. We use Mailchimp Transaction API as a library .





Some concepts are omitted. More details can be found in the publication Biundo the John - the Build a NestJS the Module for Knex.js .





Dynamic module

Dynamic module   . , John Biundo , .





dyn-schematics.





npm install @nestjsplus/dyn-schematics -g
      
      



,  





nest g -c @nestjsplus/dyn-schematics dynpkg <NAME>
      
      



,





? Generate a testing client? Yes
      
      



:





|   .npmignore
|   .prettierrc
|   nest-cli.json
|   package.json
|   README.md
|   tsconfig.build.json
|   tsconfig.json
|   tslint.json
|
\---src
    |   constants.ts
    |   index.ts
    |   mailchimp-habr.module.ts
    |   mailchimp-habr.providers.ts
    |   mailchimp-habr.service.ts
    |   main.ts
    |
    +---interfaces
    |       index.ts
    |       mailchimp-habr-module-async-options.interface.ts
    |       mailchimp-habr-options-factory.interface.ts
    |       mailchimp-habr-options.interface.ts
    |
    \---mailchimp-habr-client
            mailchimp-habr-client.controller.ts
            mailchimp-habr-client.module.ts
      
      



 package.json. npm-check-updates.  , , RxJS, ,   NestJS.





 Mailchimp Transaction API.





npm i --save @mailchimp/mailchimp_transactional
      
      



mailchimp    .    , dyn-schematics,





//mailchimp-habr-options.interface.ts
export type MailchimpHabrOptions = string;
      
      



//mailchimp-habr-options-factory.interface.ts
import { MailchimpHabrOptions } from './mailchimp-habr-options.interface';

export interface MailchimpHabrOptionsFactory {
 createMailchimpHabrOptions(): | Promise<MailchimpHabrOptions> | MailchimpHabrOptions;
}
      
      



//mailchimp-habr-module-async-options.interface.ts
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
import { MailchimpHabrOptionsFactory } from './mailchimp-habr-options-factory.interface';

import { MailchimpHabrOptions } from './mailchimp-habr-options.interface';

export interface MailchimpHabrAsyncOptions
 extends Pick<ModuleMetadata, 'imports'> {
 inject?: any[];
 useExisting?: Type<MailchimpHabrOptionsFactory>;
 useClass?: Type<MailchimpHabrOptionsFactory>;
 useFactory?: (...args: any[]) => Promise<MailchimpHabrOptions> | MailchimpHabrOptions;
}
      
      



 Symbol   MAILCHIMP_HABR_TOKEN





// constants.ts
export const MAILCHIMP_HABR_OPTIONS = Symbol('MAILCHIMP_HABR_OPTIONS');
export const MAILCHIMP_HABR_TOKEN = Symbol('MAILCHIMP_HABR_TOKEN');
      
      



 mailchimp-habr.service.ts







//mailchimp-habr.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { MAILCHIMP_HABR_OPTIONS} from './constants';
import { MailchimpHabrOptions } from './interfaces';

import * as Mailchimp from '@mailchimp/mailchimp_transactional/src';

interface IMailchimpHabrService {
 getInstance(): Mailchimp;
}

@Injectable()

export class MailchimpHabrService implements IMailchimpHabrService {
 private serviceInstance: Mailchimp;

 constructor(
   @Inject(MAILCHIMP_HABR_OPTIONS) private mailchimpHabrOptions: MailchimpHabrOptions,
 ) {}

 async getInstance(): Mailchimp {
   return this.serviceInstance ? this.serviceInstance : Mailchimp(this.mailchimpHabrOptions);
 }
}
      
      



,   .





-, @Injectable()



, NestJS, DI.   constructor , , @Inject(MAILCHIMP_HABR_OPTIONS)



  Symbol, ,   .





,     .   Dynamic Module.





dyn-schematics





//mailchimp-habr.providers.ts
import { MAILCHIMP_HABR_OPTIONS } from './constants';
import { MailchimpHabrOptions } from './interfaces';

export function createMailchimpHabrProviders(options: MailchimpHabrOptions) {
 return [
   {
     provide: MAILCHIMP_HABR_OPTIONS,
     useValue: options,
   },
 ];
}
      
      



, , ,    .     NestJS.





 NestJS Dynamic Modules .   , module: MailchimpHabrModule



.





mailchimp-habr.module.ts



,





export class MailchimpHabrModule {
  public static register(options: MailchimpHabrOptions): DynamicModule {
   return {
     module: MailchimpHabrModule,
     providers: createMailchimpHabrProviders(options),
   };
  }
}
      
      



forRoot, NestJS.





, , , exports. , , !





, , @Module



  MailchimpHabrModule



, , , ! , @Module



(forRoot/register forRootAsync/registerAsync). โ€œโ€, custom provider.





mailchimp-habr.providers.ts





// mailchimp-habr.providers.ts
export const MailchimpProvider: Provider = {
 provide: MAILCHIMP_HABR_TOKEN,
 useFactory: async (mailchimpService) => mailchimpService.getInstance(),
 inject: [MailchimpHabrService],
};
      
      



@Module



mailchimp-habr.module.ts







@Global()
@Module({
 providers: [MailchimpProvider, MailchimpHabrService],
 exports: [MailchimpProvider, MailchimpHabrService],
})
export class MailchimpHabrModule {
	/* */
}
      
      



! โ€œโ€, . ( ) mailchimp-habr.decorator.ts







//mailchimp-habr.decorator.ts
import { Inject } from '@nestjs/common';
import { MAILCHIMP_HABR_TOKEN } from './constants';

export const InjectMailchimp = () => Inject(MAILCHIMP_HABR_TOKEN);
      
      



! ! ? , .





import { Module } from '@nestjs/common';
import { MailchimpHabrClientController } from './mailchimp-habr-client.controller';
import { MailchimpHabrModule } from '../mailchimp-habr.module';

@Module({
 controllers: [MailchimpHabrClientController],
 imports: [MailchimpHabrModule.forRoot('YOUR_API_KEY')],
})
export class MailchimpHabrClientModule {}
      
      



import { Controller, Get } from '@nestjs/common';
import { InjectMailchimp } from '../mailchimp-habr.decorator';

@Controller()
export class MailchimpHabrClientController {
 constructor(@InjectMailchimp() private readonly mailchimpHabrService) {}

 @Get()
 index() {
   return this.mailchimpHabrService.users.ping();
 }
}
      
      



.





npm run start:dev
      
      



  http://localhost:3000/ ( curl, postman, insomnia...).





  ยซPONG!ยป,   api key.   ,   , this.mailchimpHabrService.users.ping();







  ?     ! !   forRoot/register   forRootAsync/registerAsync.





import { Module } from '@nestjs/common';
import { MailchimpHabrClientController } from './mailchimp-habr-client.controller';
import { MailchimpHabrModule } from '../mailchimp-habr.module';

@Module({
 controllers: [MailchimpHabrClientController],
 imports: [MailchimpHabrModule.forRootAsync({ useFactory: () => 'API_KEY' })],
})
export class MailchimpHabrClientModule {}
      
      



... !





 ,       useFactory?  NestJS โ€” ConfigModule.





 imports



  inject







import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MailchimpHabrModule } from '../mailchimp-habr.module';
import { MailchimpHabrClientController } from './mailchimp-habr-client.controller';

@Module({
 controllers: [MailchimpHabrClientController],
 imports: [
   ConfigModule.forRoot(),
   MailchimpHabrModule.forRootAsync({
   imports: [ConfigModule],
   useFactory: async (config: ConfigService) => {
     return config.get('API_KEY');
   },
   inject: [ConfigService],
 })],
})
export class MailchimpHabrClientModule {}
      
      



Happened? I think no. Most likely you got the error:





Something is wrong here. Following the documentation, we look for an error ... Looking closely at forRootAsync, we find that we do not handle imports in options in any way. Let's add.





export class MailchimpHabrModule {
  public static forRootAsync(options: MailchimpHabrAsyncOptions,): DynamicModule {
   return {
     module: MailchimpHabrModule,
     imports: options.imports ?? [], //  imports
     providers: [
       ...this.createProviders(options),
     ],
   };
  }
}
      
      



Let's try again. Everything is working!





Unit testing

NestJS supports Jest out of the box. Let's take this opportunity. Let's create a filemailchimp-habr.spec.ts







import * as Mailchimp from '@mailchimp/mailchimp_transactional/src';
import { Test } from '@nestjs/testing';
import { MAILCHIMP_HABR_TOKEN, MailchimpHabrModule } from './';

describe('Mailchimp forRoot', () => {
 let mailchimpService: Mailchimp;

 beforeEach(async () => {
   const moduleRef = await Test.createTestingModule({
     imports: [MailchimpHabrModule.forRoot('MAILCHIPM_API_KEY')],
   }).compile();

   mailchimpService = moduleRef.get<Mailchimp>(MAILCHIMP_HABR_TOKEN);
 });

 describe('mailchimpHabrService', () => {
   it('method ping exists', async () => {
     expect(mailchimpService.users.ping).toBeDefined();
   });

   it('method is a function', async () => {
     expect(mailchimpService.users.ping).toBeInstanceOf(Function);
   });

   it('users.ping() should return "PONG"', async () => {
     const result = 'PONG';
     jest
       .spyOn(mailchimpService.users, 'ping')
       .mockImplementation(() => result);

     expect(await mailchimpService.users.ping()).toBe(result);
   });
 });
});
      
      



All tests pass. Similarly, we test registration viaforRootAsync().







Outcome

Dynamic modules NestJS allows you to wrap a lot of libraries! Writing your own modules allows you to shorten the code in your projects. And publishing them to npm makes life easier not only for you.





The module is available on npm and GitHub .








All Articles