Create NestJS health check library

Reading Time: 6 minutes

 91 total views,  2 views today

Introduction

NestJS applications in my company implement health checking for k8s liveness probe. These applications check the following items are up and running: backend, database, redis, bull queue and external servers. Eventually, different backend teams encapsulate similar endpoint and services in a heath check module and import it to the application. This approach is not DRY and I decided to create a heath check library and reuse it in applications to replace the custom health check module.

In this blog post, I show the codes to create a nestjs health check library and use it in a sample application to check the health of backend, Postgres database, redis, bull queues and doc website of nestjs and angular.

let's go

Create health check library

First, create a new project called nestjs-health. Rename src/ to lib/ and update tsconfig.json and package.json respectively.

// Directory tree
lib
├── controllers
│   ├── health.controller.ts
│   └── index.ts
├── health-module-definition.ts
├── health.module.ts
├── index.ts
├── indicators
│   ├── bull-queue.health.ts
│   └── index.ts
├── interfaces
│   ├── health-module-options.interface.ts
│   └── index.ts
├── services
│   ├── health.service.ts
│   └── index.ts
└── types
    ├── index.ts
    └── redis-connection-options.type.ts
// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./lib",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "esModuleInterop": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["lib/**/*"],
  "exclude": ["dist", "test", "node_modules"]
}

Update outDir to ./dist and rootDir to ./lib. Include ['lib/**/*'] and excludes ["dist", "test", "node_modules"] from compilation

Add "main": "./dist/index.js" to package.json

Install dependencies

npm i --save-exact --save-dev @nestjs/terminus
npm i --save-exact @nestjs/microservices @nestjs/axios @nestjs/bull bull @nestjs/typeorm ts-node

Then, move some “dependencies” to “peerDependencies”

"peerDependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/microservices": "^9.1.5",
    "@nestjs/axios": "^0.1.0",
    "@nestjs/bull": "^0.6.1",
    "bull": "^4.10.1",
    "@nestjs/typeorm": "^9.0.1",
    "ts-node": "^10.0.0"
}

Define HealthModuleOptions interface

First, we define HealthModuleOptions interface that enables us to pass configurations from application to the library

// redis-connection-options.type.ts

import { RedisOptions } from '@nestjs/microservices';

export type RedisConnectOptions = RedisOptions['options'];
// health-module-options.interface.ts

import { HealthIndicatorFunction } from '@nestjs/terminus';
import { RedisConnectOptions } from '../types';

export interface HealthModuleOptions {
  app: string;
  backendUrl: string;
  shouldCheckDatabase?: boolean;
  queueNames?: string[];
  redisOptions?: RedisConnectOptions;
  indicatorFunctions?: HealthIndicatorFunction[];
}
  • app – key of the backend application
  • backendUrl: URL of the backend application, for example, http://localhost:3000
  • shouldCheckDatabase: determine whether or not ping the database that uses TypeORM
  • queueNames: Name array of bull queues
  • indicatorFunctions: custom indicator functions designed to check components’ health

Define Health Module Definition

Define a ConfigurableModuleBuilder to export ConfigurationModuleClass and MODULE_OPTIONS_TOKEN.

Invoke setClassMethodName to override the static methods of HealthModule to forRoot and forRootAsync

import { ConfigurableModuleBuilder } from '@nestjs/common';
import { HealthModuleOptions } from './interfaces';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<HealthModuleOptions>()
  .setClassMethodName('forRoot')
  .build();

Create module for the health check library

Create HealthModule that extends ConfigurableModuleClass

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { ConfigurableModuleClass } from './health-module-definition';

@Module({
  imports: [TerminusModule],
  controllers: [],
  providers: [],
  exports: [],
})
export class HealthModule extends ConfigurableModuleClass {}

controllers, providers and exports are empty arrays but we will update them after generating services and controller.

Generate custom health indicator

The following code perform health check on arbitrary number of bull queues. A queue is up and can accept jobs when it connects to redis and redis status is ready.

// indicators/bull-queue.health.ts

import { getQueueToken } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { Queue } from 'bull';

@Injectable()
export class BullQueueHealthIndicator extends HealthIndicator {
  constructor(private moduleRef: ModuleRef) {
    super();
  }

  async isHealthy(queues: string[]): Promise<HealthIndicatorResult> {
    const promiseResults = await this.checkQueuesReady(queues);
    const errorResults = this.filterErrors(promiseResults);

    if (errorResults.length) {
      throw new HealthCheckError('Bull queue failed', this.getStatus('bull', false, { errors: errorResults }));
    }

    return this.getStatus('bull', true);
  }

  private filterErrors(promiseResults: PromiseSettledResult<boolean>[]): string[] {
    const errorResults: string[] = [];
    for (const promiseResult of promiseResults) {
      if (promiseResult.status === 'rejected') {
        if (promiseResult.reason instanceof Error) {
          errorResults.push(promiseResult.reason.message);
        } else {
          errorResults.push(promiseResult.reason);
        }
      }
    }
    return errorResults;
  }

  private async checkQueuesReady(queues: string[]) {
    const promises = queues.map(async (name) => {
      const queueToken = this.moduleRef.get(getQueueToken(name), { strict: false });
      if ((queueToken as Queue).isReady) {
        const queue = await (queueToken as Queue).isReady();
        const isEveryClientReady = queue.clients.every((client) => client.status === 'ready');
        if (!isEveryClientReady) {
          throw new Error(`${name} - some redis clients are not ready`);
        }
        return true;
      }
      throw new Error(`${name} is not a bull queue`);
    });

    return Promise.allSettled(promises);
  }
}

Create health checking service

In the health service, I inject MODULE_OPTIONS_TOKEN injection token to obtain a reference to HealthModuleOptions. Then, I examine the options to determine the components to perform health check.

// services/health.service.ts

import { MODULE_OPTIONS_TOKEN } from '../health-module-definition';
import { HealthModuleOptions } from '../interfaces';
import { BullQueueHealthIndicator } from '../indicators';

@Injectable()
export class HealthService {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: HealthModuleOptions,
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator,
    private bull: BullQueueHealthIndicator,
    private microservice: MicroserviceHealthIndicator,
  ) {}

  check(): Promise<HealthCheckResult> {
    const indicatorFunctions: HealthIndicatorFunction[] = [];

    indicatorFunctions.push(() => this.http.pingCheck(this.options.app, this.options.backendUrl));

    if (this.options.shouldCheckDatabase) {
      indicatorFunctions.push(() => this.db.pingCheck('database'));
    }

    if (this.options.redisOptions) {
      indicatorFunctions.push(() =>
        this.microservice.pingCheck<RedisOptions>('redis', {
          transport: Transport.REDIS,
          options: this.options.redisOptions,
          timeout: 5000,
        }),
      );
    }

    if (this.options.queueNames?.length) {
      indicatorFunctions.push(() => this.bull.isHealthy(this.options.queueNames));
    }

    if (this.options.indicatorFunctions?.length) {
      indicatorFunctions.push(...this.options.indicatorFunctions);
    }

    return this.health.check(indicatorFunctions);
  }
}

Create health controller

The controller has one endpoint, /health, that injects HeathService to check the readiness of the components.

import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckResult } from '@nestjs/terminus';
import { HealthService } from '../services';

@Controller('health')
export class HealthController {
  constructor(private health: HealthService) {}

  @Get()
  @HealthCheck()
  check(): Promise<HealthCheckResult> {
    return this.health.check();
  }
}

Register controller and services

Now, we register new controller and services in HealthModule

// heath.module.ts

import { Module } from '@nestjs/common';
... other import statements ...
import { HealthService } from './services';
import { HealthController } from './controllers';
import { BullQueueHealthIndicator } from './indicators';

@Module({
  imports: [TerminusModule],
  controllers: [HealthController],
  providers: [HealthService, BullQueueHealthIndicator],
  exports: [HealthService],
})
export class HealthModule extends ConfigurableModuleClass {}

Finally, add index.ts to export controllers, interfaces, services, types and the health module

./index.ts

export * from './interfaces';
export * from './services';
export * from './health.module';
export * from './types';

I finished the implementation of the library and will publish it to npmjs.com. Npm publish is not part of the scope of this article.

Use case of health checking

I would continue to use nestjs-health-terminus project to demonstrate how to health check the following resources:

  • backend app
  • Postgres database that uses TypeORM
  • redis
  • bull queues
  • just for fun, ping docs.nestjs.com and angular.io/docs are alive

Clone or fork the Nestjs project

You can find the sample code of nestjs bull queue in this repo: https://github.com/railsstudent/nestjs-health-terminus

First, copy .env.example to .env to load environment variables of redis and Postgres database

REDIS_PORT=6379
REDIS_HOST=localhost
PORT=3000
NODE_DEV=development
BACKEND_DOMAIN=http://localhost:3000
DATABASE_CONNECT=postgres
DATABASE_HOST=localhost
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=testDB
DATABASE_PORT=5432

Second, run docker-compose to create and run redis and Postgres

docker-compose up -d

Install dependencies

Install nestjs-health library

npm install --save-exact nestjs-health

Import health module to AppModule

First, create health.config.ts in src/configs folder

import { ConfigService } from '@nestjs/config';
import { HttpHealthIndicator } from '@nestjs/terminus';
import { HealthModule } from 'nestjs-health';

export const healthConfig = HealthModule.forRootAsync({
  inject: [ConfigService, HttpHealthIndicator],
  useFactory: (configService: ConfigService, http: HttpHealthIndicator) => {
    return {
      app: 'nestjs-health-terminus',
      backendUrl: configService.get<string>('BACKEND_DOMAIN', ''),
      shouldCheckDatabase: true,
      queueNames: ['fibonacci', 'prime'],
      redisOptions: {
        host: configService.get<string>('REDIS_HOST', 'localhost'),
        port: configService.get<number>('REDIS_PORT', 0),
      },
      indicatorFunctions: [
        () => http.pingCheck('nestjs-docs', 'https://docs.nestjs.com'),
        () => http.pingCheck('angular-docs', 'https://angular.io/docs'),
      ],
    };
  },
});

heathConfig variable configures dependent resources that require health checking.

Next, import healthConfig in AppModule

@Module({
  imports: [
    ... other modules...
    healthConfig,
  ]
})
export class AppModule {}

Invoke health check endpoint

Run docker-compose to start redis. Since both fibonacci and prime queues can connect to redis, the health check status should be “up”

http://localhost:3000/health

Kill redis and call the same endpoint to display a different response.

docker stop <container id of redis>

redis and bull display health check error messages.

Kill Postgres and call the same endpoint to display a new response.

docker stop <container id of Postgres>

Final Thoughts

In this post, I show how to author a NestJS library to encapsulate health check services and controller in a dynamic module. Then, I publish the library to npmjs and install it into NestJS application to check the health of backend application, database, redis, bull queues and external servers. Finally, the library provides a defined /health endpoint that displays the status of components.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. NestJS Heath Library: https://github.com/railsstudent/nestjs-health
  2. Repo: https://github.com/railsstudent/nestjs-health-terminus
  3. npm package: https://www.npmjs.com/package/nestjs-health
  4. Terminus: https://docs.nestjs.com/recipes/terminus
  5. Dynamic Module: https://docs.nestjs.com/fundamentals/dynamic-modules#dynamic-modules

Check health of nestjs bull queue with terminus

Reading Time: 5 minutes

 72 total views,  1 views today

Introduction

It is common for enterprise applications to connect to external resources to process requests and get back responses. Therefore, application should know that the components it depends on are up and running all the times. Otherwise, the application behaves erroneously when serving client requests. To stay aware of the availability of connected resources, we can perform health check on them and wait for the results.

In this blog post, I describe the use case of health check bull queue in nestjs with terminus. First, it requires a custom health indicator to verify that the queue is connected to redis and redis status is ready. Next, I add a /health endpoint that calls the health indicator to return the status.

let's go

Clone or fork the Nestjs project

You can find the sample code of nestjs bull queue in this repo: https://github.com/railsstudent/nestjs-health-terminus

nest generate nestjs-health-terminus

Install dependencies

Install nestjs terminus library to the project

npm install --save @nestjs/terminus

Create health module for health check

nest g mo health

Import HealthModule in AppModule

@Module({
  imports: [
    ... other modules...
    HealthModule,
  ]
})
export class AppModule {}

Add a custom health indicator to health check bull queue

@nestjs/terminus does not provide built-in health indicator for bull queue; therefore, I create a custom one in the health module.

First, Use nestjs CLI to generate a health indicator

nest g s health/health/bullQueue --flat

My convention is to rename the filename suffix from .service.ts to .health.ts.

src/health
├── controllers
│   ├── health.controller.ts
│   └── index.ts
├── health
│   ├── bull-queue.health.ts
│   └── index.ts
├── health.module.ts
└── index.ts

Then, open bull-queue.health.ts and extends BullQueueHealthIndicator from HealthIndicator.

import { getQueueToken } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { Queue } from 'bull';

@Injectable()
export class BullQueueHealthIndicator extends HealthIndicator {
  constructor(private moduleRef: ModuleRef) {
    super();
  }
}

Next, import TerminusModule into HealthModule and verify that the providers array has the entry of BullQueueHealthIndicator.

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './controllers';
import { BullQueueHealthIndicator } from './health';

@Module({
  imports: [TerminusModule],
  controllers: [HealthController],
  providers: [BullQueueHealthIndicator],
})
export class HealthModule {}

Add health check logic in custom health indicator

In BullQueueHealthIndicator, I implement isHealthy() method to check the health of fibonacci and prime queues. When the queue connects to redis and redis status is ready, it is considered up. Otherwise, the queue is down and the indicator returns error messages.

async isHealthy(queues: string[]): Promise<HealthIndicatorResult> {
    const promiseResults = await this.checkQueuesReady(queues);
    const errorResults = this.filterErrors(promiseResults);

    if (errorResults.length) {
      throw new HealthCheckError('Bull queue failed', this.getStatus('bull', 
         false, { errors: errorResults }));
    }

    return this.getStatus('bull', true);
 }

checkQueuesReady accepts a list of queue names and examines their readiness.

private async checkQueuesReady(queues: string[]) {
    const promises = queues.map(async (name) => {
      const queueToken = this.moduleRef.get(getQueueToken(name), 
          { strict: false });
      if ((queueToken as Queue).isReady) {
        const queue = await (queueToken as Queue).isReady();
        const isEveryClientReady = queue.clients.every((client) => client.status === 'ready');
        if (!isEveryClientReady) {
          throw new Error(`${name} - some redis clients are not ready`);
        }
        return true;
      }
      throw new Error(`${name} is not a bull queue`);
    });

    return Promise.allSettled(promises);
}

const queueToken = this.moduleRef.get(getQueueToken(name), { strict: false });

The code uses the queue token to obtain the queue instance. When the queue is ready, I get queue.clients that is an array of redis clients. When every redis client has ready status, the queue is ready to accept jobs. On the other hand, when redis store is not set up, the method throws an error message.

private filterErrors(promiseResults: PromiseSettledResult<boolean>[]): string[] {
    const errorResults: string[] = [];
    for (const promiseResult of promiseResults) {
      if (promiseResult.status === 'rejected') {
        if (promiseResult.reason instanceof Error) {
          errorResults.push(promiseResult.reason.message);
        } else {
          errorResults.push(promiseResult.reason);
        }
      }
    }
    return errorResults;
 }

The result of checkQueuesReady is a collection of resolved and rejected promises. filterErrors iterates the promise results, finds those that are rejected and stores the error messages.

When health check returns error messages, isHealthy throws HealthCheckError.

throw new HealthCheckError('Bull queue failed', this.getStatus('bull', false, 
    { errors: errorResults }));

When health check does not find error, isHealthy returns

this.getStatus('bull', true);

Define health controller

nest g co health/controllers/health --flat

import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
import { BullQueueHealthIndicator } from '../health/bull-queue.health';

@Controller('health')
export class HealthController {
  constructor(private health: HealthCheckService, private bull: BullQueueHealthIndicator) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime'])]);
  }
}

First, the GET endpoint is annotated with a HealthCheck decorator. Second, the HealthCheckService expects some health indicators to execute. Therefore, I provide BullQueueHealthIndicator to the check method to process.

this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime'])])

checks the health of fibonacci and prime queues and maps the health status to the object with key “bull”.

Invoke health check endpoint

Run docker-compose to start redis. Since both fibonacci and prime queues can connect to redis, the health check status should be “up”

http://localhost:3000/health

Kill docker-compose and call the same endpoint to see a different response.

Both queues are down because redis has not launched yet.

Suppose I also check ‘dummy’ that is not a bull queue, isHealthy outputs bad injection token error message.

return this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime', 'dummy'])]);

Final Thoughts

In this post, I show how to use terminus package to write a custom health indicator to check the health of bull queue. When terminus package does not provide predefined indicators and there is no open source library, we have the ability to write our own indicators to perform the appropriate testing.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. Repo: https://github.com/railsstudent/nestjs-health-terminus
  2. Queue: https://docs.nestjs.com/techniques/queues
  3. Bull: https://github.com/OptimalBits/bull
  4. Terminus: https://docs.nestjs.com/recipes/terminus

Queue and job processing in nestjs

Reading Time: 5 minutes

 99 total views

Introduction

The responsibility of backend application is to handle client requests and return responses back to the client. However, backend performance can degrade when the request is resource intensive; UI thread waits endlessly and user interface is blocked for further user actions. To avoid the unresponsiveness, we can place jobs in queue and delegate the processor to manage job processing.

In this blog post, I describe how to register bull queues in a Nestjs project and add jobs to queue to run in the backend. When processor consumes the job from the queue and finishes job processing, the queue listener logs message to terminal.

let's go

Create a new Nestjs project

nest generate nestjs-health-terminus

Install dependencies

Install bull queue package for nestjs and other dependencies for configuration and validation.

npm i @nestjs/bull bull
npm i @nestjs/config class-validator class-transformer
npm i --save-dev @types/bull

Add Docker Compose to install redis

version: '3.1'

services:
  redis:
    container_name: redis
    image: redis:6-alpine
    restart: always
    ports:
      - '6379:6379'
    volumes: 
      - cache:/data
    networks:
      - terminus

volumes:
  cache:

networks:
  terminus:
    driver: bridge

When we launch docker compose, it will install and run Redis 6 on port number 6379.

Import Bull module used by queues

//.env
REDIS_PORT=6379
REDIS_HOST=localhost
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    BullModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        redis: {
          host: configService.get<string>('REDIS_HOST'),
          port: configService.get<number>('REDIS_PORT'),
        },
      }),
    }),
  ]
})
export class AppModule {}

Register bull queues to process jobs

To enable configService to find the values of REDIS_HOST and REDIS_PORT, the project needs to provide a .env with the environment variables.

// .env

REDIS_HOST=localhost
REDIS_PORT=6379

In tutorial, it is tempting to hardcode the values but professional developers should not do it in production application.

First, we create a queue module to demonstrate some examples of queue and process jobs.

import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { FibonacciService, PrimeService } from './services';
import { QueueController } from './controllers';
import { PrimeProcessor, FibonacciProcessor } from './processors';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'fibonacci',
    }),
    BullModule.registerQueue({
      name: 'prime',
    }),
  ],
  providers: [FibonacciService, FibonacciProcessor, PrimeProcessor, PrimeService],
  controllers: [QueueController],
})
export class QueueModule {}

The module is consisted of the following

  • Two bull queues, fibonacci and prime, respectively
  • Queue controller that adds jobs to fibonacci and prime queues
  • FibonacciService that calculates the first Nth fibonacci numbers
  • FibonacciProcessor that processes jobs in fibonacci queue
  • PrimeService that solves two problems of prime factors
  • PrimeProcessor that processes “prime-factors” and “distinct-prime-factors” jobs

Add jobs to queue and wait for job processing

I inject queues to add job in the controller in my example but queuing can also occur in services. If the background job depends on the previous results in the service, then we will inject the queue into the service and append jobs to it inside the method.

@Controller('queue')
export class QueueController {
  constructor(
    @InjectQueue('fibonacci') private fibonacciQueue: Queue<{ order: number }>,
  ) {}

  @Post('fib')
  async getFibonacci(@Query('order', ParseIntPipe) order: number): Promise<void>   {
    console.log(`${new Date()} - Job submitted to queue`, order);
    await this.fibonacciQueue.add({ order }, { delay: 1000 });
  }
}

Fibonacci queue has a single job that is to calculate the first Nth fibonacci numbers a

await this.fibonacciQueue.add({ order }, { delay: 1000 });

adds job with data { order } and delays by one second ({ delay: 1000 }).

Add named jobs to queue for job processing

@Controller('queue')
export class QueueController {
  constructor(@InjectQueue('prime') private primeQueue: Queue) {}


  @Post('prime-factors')
  async getPrimeFactors(@Query('input', ParseIntPipe) input: number): Promise<void> {
    console.log(`${new Date()} - Prime factors job submitted to prime queue`, input);
    await this.primeQueue.add('prime-factors', { input }, { delay: 1000 });
  }

  @Post('distinct-prime-factors')
  async getDistinctPrimeFactors(@Body() arrayDto: ArrayProductsDto): Promise<void> {
    console.log(`${new Date()} - Distinct prime factor job submitted to prime queue`, arrayDto.products);
    await this.primeQueue.add('distinct-prime-factors', {
       products: arrayDto.products,
    });
  }
}

On the other hand, prime queue manages two types of jobs. Therefore, we specify the job name when appending job to the queue.

await this.primeQueue.add('prime-factors', { input }, { delay: 1000 });

The code snippet appends ‘prime-factors’ job to queue to find all prime factors of an integer

await this.primeQueue.add('distinct-prime-factors', {
   products: arrayDto.products,
});

The other code snippet appends ‘distinct-prime-factors’ job to queue to find distinct prime factors of an integer.

Next, we define job processors to process the jobs such that they don’t stay in idle status in the queues.

Define job processors for job processing

It is really easy to create job processors in nestJs. Job processor is a class annotated by @Processor() decorator and queue name. Each method has a @process() decorator and an optional job name to consume queue job.

@Processor('fibonacci')
export class FibonacciProcessor {
  constructor(private fibonacciService: FibonacciService) {}

  @Process()
  calculateNFibonacciNumbers({ data }: Job<{ order: number }>): void {
    const fibSequences = this.fibonacciService.fibonacci(data.order);
    console.log(`${new Date()} Calculating ${data.order + 1} fibonacci numbers...`);
    for (let i = 0; i < data.order; i++) {
      console.log(`${new Date()} - Fib(${i}) = ${fibSequences[i]}`);
    }
  }
}

@Processor('fibonacci') listens to fibonacci queue registered in queue module. The method, calcualteNFibonacciNumbers, is responsible for all jobs of fibonacci queue because @Process() decorator does not specify a job name. In my opinion, the method will violate single responsibility principle if it processes all types of jobs of the queue.

Define specialized job method for job processing

If we want a process method to take care of a single type of job, @Process() decorator accepts name parameter that represents job name.

import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { PrimeService } from '../services';

@Processor('prime')
export class PrimeProcessor {
  constructor(private primeService: PrimeService) {}

  @Process('prime-factors')
  findPrimeFactors({ data }: Job<{ input: number }>): void {
    const primeFactors = this.primeService.getPrimeFactors(data.input);
    console.log(`${new Date()} - All prime factors`, primeFactors);
  }

  @Process('distinct-prime-factors')
  findDistinctPrimeFactors({ data }: Job<{ products: number[] }>): void {
    const { primeFactors, count } = this.primeService.getDistinctPrimeFactors(data.products);
    console.log(`${new Date()} - Distinct prime factors`, primeFactors, 'count:', count);
  }
}

findPrimeFactors receives 'prime-factors' job and determines all prime factors of an integer. Similarly, findDistinctPrimeFactors receives 'distinct-prime-factors' job and determines distinct prime factors and the number of them. It is a clean approach than if-then-else to execute different methods of primeService to return the expected results.

Listen to queue events during the lifecycle of job processing

Queue events of Bull offer pre-process and post-process hooks for developers to perform custom actions such as logging and caching. When the events are local, they must implement within the processor such as PrimeProcessor.

The code here implements OnQueueActive and OnQueueCompleted to log start and end time of the job.

@Processor('prime')
export class PrimeProcessor {
  constructor(private primeService: PrimeService) {}

 ...

 @OnQueueActive()
 onJobActive(job: Job) {
    console.log(
      `onJobActive - id: ${job.id}, name: ${job.name}, data: `,
      job.data,
      ` starts at ${new Date(job.timestamp)}`,
    );
 }

 @OnQueueCompleted()
 onJobSuccess(job: Job, result: any) {
    console.log(
      `onJobSuccess - id: ${job.id}, name: ${job.name}, data: `,
      job.data,
      ` completes at ${new Date(job.finishedOn)}`,
      'result',
      result,
    );
 }

Execute cURL request

curl --location --request POST 'http://localhost:3000/queue/prime-factors?input=88'

Console log output:

onJobActive - id: 1, name: prime-factors, data:  { input: 88 }  starts at Sat Sep 17 2022 11:20:54 GMT+0800 (Hong Kong Standard Time)
onJobSuccess - id: 1, name: prime-factors, data:  { input: 88 }  completes at Sat Sep 17 2022 11:20:55 GMT+0800 (Hong Kong Standard Time) result undefined

Final Thoughts

In this post, we show a comprehensive bull queue example in Nestjs. When an application has a task that is expected to run a long time, we have the option to place it in the queue to process somewhere. Then, we do not block the main event loop and ensure UI is responsive for further user actions.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. Queue: https://docs.nestjs.com/techniques/queues
  2. Bull: https://github.com/OptimalBits/bull
  3. Repo: https://github.com/railsstudent/nestjs-health-terminus

Lint files and dependencies in NestJs with unimported

Reading Time: 5 minutes

 105 total views

Introduction

When developers maintain a NestJS project, they add and delete codes to the project that some dependencies and files become redundant. When unused files and dependencies are left around, they become technical debts and reduce the code quality. Fortunately, we can use unimported to lint files and dependencies before git commit to avoid pushing unused files and dependencies to the code base.

In this blog post, I describe how to execute unimported binary to add .unimportedrc.json to a Nestjs project and create a pre-commit hook to lint files and dependencies of the projects during git commit.

let's go

Create a new Nestjs project

nest generate test-app

Add .unimportedrc.json to lint files and dependencies

First, we execute unimported binary to initialize .unimportedrc.json to the project

npx unimported --init

We will find a barebone .unimportedrc.json after the initialization.

{
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

Most of the Nestjs projects that I work on use TypeOrm to generate migration scripts and interface with PostgreSQL; therefore, I want unimported to ignore **/migrations/** folder in ignorePatterns array.

{
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts",
    "**/migrations/**"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

NestJs is nodejs under the hood and unimported also supports a node preset.

npx unimported --show-preset        // show all available presets
npx unimported --show-preset node   // shows the json file of node preset

The following is the output of the configuration:

{
  preset: 'node',
  entry: [ 'src/main.ts' ],
  extensions: [ '.js', '.jsx', '.ts', '.tsx' ],
  flow: false,
  ignorePatterns: [
    '**/node_modules/**',
    '**/*.tests.{js,jsx,ts,tsx}',
    '**/*.test.{js,jsx,ts,tsx}',
    '**/*.spec.{js,jsx,ts,tsx}',
    '**/tests/**',
    '**/__tests__/**',
    '**/*.d.ts'
  ],
  ignoreUnimported: [],
  ignoreUnresolved: [],
  ignoreUnused: []
}

We copy preset, entry and extensions to the config file in the project and the configuration file finally completes.

{
  "preset": "node",
  "entry": [
    "src/main.ts"
  ],
  "extensions": [
    ".js",
    ".jsx",
    ".ts",
    ".tsx"
  ],
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts",
    "**/migrations/**"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

Create pre-commit hook to perform unimported check

First, we install husky devDependencies and add prepare script in package.json.

npm install husky --save-dev --save-exact
npm set-script prepare "husky install"
npm run prepare

Then, we add a pre-commit husky hook

npx husky add .husky/pre-commit "npx --no unimported"
git add .husky/pre-commit
git commit -m "chore: test unimported check"

If we see the follow output in the terminal, pre-commit hook has successfully performed unimported check.

✓ There don't seem to be any unimported files.
[master f33fe57] chore: test unimported check
 1 file changed, 2 insertions(+), 1 deletion(-)

Up to this point, we focus on setting up code quality tools for the project. Now, we are ready to demonstrate the power of unimported by adding unused file(s) and dependencies. In the demo, husky hook aborts whenever we commit the source codes and unimported prints the report card in the terminal. Then, we systematically modify the codes until we satisfy unimported check and commit the changes.

Demo unimported to lint files and dependencies

The demo is to add configuration and .env validation in Nestjs according to the official documentation https://docs.nestjs.com/techniques/configuration.

First, install dependencies

npm i --save-exact @nestjs/config dotenv joi
git add .
git commit -m "chore(deps): install dependencies"
       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 3
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 3 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ @nestjs/config
   2  │ dotenv
   3  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       Inspect the results and run npx unimported -u to update ignore lists
husky - pre-commit hook exited with code 1 (error)

We have 3 unused dependencies but @nestjs/config will go away when we import ConfigModule in AppModule.

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
git add .
git commit -m "chore(deps): install dependencies"

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 2
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 2 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
   2  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

We have resolved 1 unused dependency and 2 remain. Next, we use joi to validate the schema of .env.

Let’s create a new file to define the schema of .env file

touch src/validation-schema.ts

Then, we copy and paste the schema to the TypeScript file

// validation-schema.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test', 'provision')
    .default('development'),
  PORT: Joi.number().default(3000),
});

If we commit the codes now, unimported will detect one unused file and 2 unused dependencies.

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 2
       unimported files    : 1

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 2 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
   2  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 1 unimported files
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ src/validation-schema.ts
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

It seems umimported report card is worse than before but it is alright. We forgot to import validation-schema.ts in app.module.ts and pass the validationSchema object to ConfigModule.

Update app.module.ts and commit again

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { validationSchema } from './validation-schema';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 1
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 1 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Why is dotenv unused? I referred to package.json of @nestjs/config and found out that dotenv is a dependency of the library. I don’t require to install dotenv in the project and should remove it.

We can make it happen by running npm uninstall dotenv. Let’s commit the codes and not keep the suspense further.

git add .
connie@connie-ThinkPad-T450s ~/Documents/ws_react_graphql_nest/test-app $ git commit -m "chore(deps): install dependencies and add configuration"
✓ There don't seem to be any unimported files.
[redo-demo 44cbf8d] chore(deps): install dependencies and add configuration
 5 files changed, 6 insertions(+), 7 deletions(-)
 rename src/{validationSchema.ts => validation-schema.ts} (100%)

We have finally committed the changes and remove dotenv dependency that the project does not require.

Final Thoughts

In this post, we see how easy it is leave unused files and dependencies in the code base. Fortunately, we can apply unimported command in pre-commit hook to lint files and dependencies before code commit. When Nestjs project has unused files and dependences, code commit fails until we resolve unimported errors. This way, we can keep clean code and avoid introducing technical debts that the successors have to deal with.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. unimported: https://www.npmjs.com/package/unimported
  2. husky: https://www.npmjs.com/package/husky

Create dynamic module made easy in NestJS 9

Reading Time: 4 minutes

 112 total views

Introduction

Our technology team builds a lot of Nestjs libraries to reuse in Nestjs applications. Our libraries provide forRoot() to create dynamic module only. Therefore, we cannot call forRootAsync() to register asynchronous module by importing modules and injecting services.

Before V9, there are many boilerplate codes to get forRootAsync() to work. Internally, forRootAsync invokes createAsyncProvider and createAsyncOptionsProviders private static methods and both of them accept some sort of ModuleAsyncOptions interface. As the result, the team avoids the implementation unless it is absolutely required.

NestJS 9 introduces ConfiguratableModuleBuilder and it simplifies the steps to create dynamic module by eliminating most of the boilerplate codes.

In this blog post, I describe how to create a nestjs library that provides forRoot and forRootAsync with ConfigurableModuleBuilder. Then, I can install it in a new nestjs 9 app to register the module either synchronously or asynchronously.

let's go

Upgrade NestJS to V9

npm install -g @nestjs/cli npm-check-updates
nest update
npm-check-updates "/@nestjs*/" -u

Create a new nestjs library

nest new nestjs-world-cup-lib

The example is a dummy library to return World Cup finals from 1930 to 2018.

Rename src/ to lib/ and update tsconfig.json accordingly

nestjs-world-cup-lib
├── README.md
├── cspell.json
├── lib
│   ├── enums
│   │   ├── countries.enum.ts
│   │   └── index.ts
│   ├── index.ts
│   ├── interfaces
│   │   ├── index.ts
│   │   ├── world-cup-module-options.interface.ts
│   │   └── world-cup-result.interface.ts
│   ├── services
│   │   ├── index.ts
│   │   └── world-cup.service.ts
│   ├── world-cup-module-definition.ts
│   └── world-cup.module.ts
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json
// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./lib",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "strict": true,
    "strictPropertyInitialization": false,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["lib/**/*"],
  "exclude": ["dist", "test", "node_modules"]
}

Update outDir to ./dist and rootDir to ./lib. Include ['lib/**/*'] and excludes ["dist", "test", "node_modules"] from compilation

Steps to create dynamic module

  • Create an interface for the options
  • Instantiate ConfigurableModuleBuilder and export ConfigurableModuleClass and MODULE_OPTIONS_TOKEN
  • Generate providers, services, etc that register and export from the new module
  • Add the configurable module that extends ConfigurableModuleClass
import { ConfigurableModuleBuilder } from '@nestjs/common'
import { WorldCupModuleOptions } from './interfaces'

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<WorldCupModuleOptions>()
  .setClassMethodName('forRoot')
  .build()

Step 1: Create an interface for the options

The simple interface has 2 options: the year World Cup is held and your favorite world cup team

// world-cup-module-options.interface.ts

export interface WorldCupModuleOptions {
  year: number
  favoriteCountry: string
}

Step 2: Instantiate ConfigurableModuleBuilder

This step is identical for every dynamic module. We instantiate ConfigurableModuleBuilder and export ConfigurableModuleClass and MODULE_OPTIONS_TOKEN

// world-cup-module-definition.ts

import { ConfigurableModuleBuilder } from '@nestjs/common'
import { WorldCupModuleOptions } from './interfaces'

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<WorldCupModuleOptions>()
  .setClassMethodName('forRoot')
  .build()

ConfigurableModuleBuilder generates register and registerAsync by default. In my example, I want to use forRoot and forRootAsync instead. Therefore, I override the configuration by setClassMethodName('forRoot').

Step 3: Generate a service and export from module

// world-cup.service.ts

@Injectable()
export class WorldCupService {
  constructor(@Inject(MODULE_OPTIONS_TOKEN) private options: WorldCupModuleOptions) {}

  getYear(): number {
    return this.options.year
  }
}

The service injects MODULE_OPTIONS_TOKEN in the constructor to access options. We can access the values by this.options.year and this.options.favoriteCountry respectively.

Add the configurable module that extends ConfigurableModuleClass

import { Module } from '@nestjs/common'
import { WorldCupService } from './services'
import { ConfigurableModuleClass } from './world-cup-module-definition'

@Module({
  providers: [WorldCupService],
  exports: [WorldCupService],
})
export class WorldCupModule extends ConfigurableModuleClass {}

The new module extends ConfigurableModuleClass but we can register and export services the same way as before.

The implementation of the library is done, we can publish it to npmjs and try it in a new nestjs V9 application

Register dynamic module and test in nestjs V9 application

npm i nestjs-world-cup-lib
import { WorldCupModule } from 'nestjs-world-cup-lib'

@Module({
  imports: [
    /* Register synchronous dynamic module */
    WorldCupModule.forRoot({
       year: 2018,
       favoriteCountry: 'Brazil'
    })    

    /* Register asynchronous dynamic module
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        YEAR: Joi.number().optional(),
        FAVORITE_COUNTRY: Joi.string().optional(),
      }),
    }),
    WorldCupModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (service: ConfigService) => ({
        year: service.get<number>('YEAR'),
        favoriteCountry: service.get<string>('FAVORITE_COUNTRY'),
      })
    })
     */
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Test WorldCupService in AppController

import { WorldCupService } from 'nestjs-world-cup-lib'

@Controller()
export class AppController {
  constructor(private worldCupService: WorldCupService) {}

  @Get('world-cup-year')
  getWorldCupYear(): number {
    return this.worldCupService.getYear()
  }
}

Type http://localhost:3000/world-cup-year in the browser to verify the endpoint.

Final Thoughts

In this post, we see one of the new changes of nestjs v9 that is ConfigurableModuleBuilder. ConfigurableModuleBuilder class simplify the process to create dynamic module. When dynamic module can register without dependency, register, forRoot and forFeature methods are suffice. Alternatively, dynamic module that depends on dependency to initialize configurable options should use registerAsync, forRootAsync and forFeatureAsync counterparts.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder
  2. nestjs-world-cup-lib repo: https://github.com/railsstudent/nestjs-world-cup-lib
  3. Source code of ConfigurableModuleBuilder – https://github.com/nestjs/nest/blob/master/packages/common/module-utils/configurable-module.builder.ts
  4. How to update nestjs: https://blog.bitsrc.io/how-to-update-nest-js-ecd0e9466165

Author nestjs library using Angular Schematics

Reading Time: 7 minutes

 101 total views

Introduction

When I create nestjs project to do proof of concept, I usually spend the first hour to install dependencies and add rules to eslintrc.js before writing any meaningful code. When I researched code generation in nestjs, I found out that angular schematics can author nestjs library to upgrade configurations and add new files. Therefore, I decide to design a library that updates prettier configuration and generates eslint prettier rule using schematics.

In this blog post, I author a nestjs library, test the library on nestjs application locally and publish it to npmjs.com.

let's go

Install essential dependencies to author nestjs library

npm install -g @angular-devkit/schematics-clinpm install --save-dev --save-exact cpx@1.5.0 lodash@4.17.21 rimraf@3.0.2 typescript@4.7.2
npm install --save-exact @angular-devkit/core@13.3.0 @angular-devkit/schematics@13.3.0 @schematics/angular@13.3.0

Generate new schematics project to author nestjs library

schematics blank --name=nest-prettier

My habit is to rename src to schematics

nest-prettier
├── package.json
├── package-lock.json
├── README.md
├── schematics
│   ├── collection.json
│   └── nest-prettier
│       ├── index_spec.ts
│       └── index.ts
└── tsconfig.json

Update collection.json

As the name implies, collection.json is a collection of schematics in the library. In this library, I have one schematic that is nest-add that I can install via

nest add nest-prettier

to apply changes within the the latest version of nestjs application

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "nest-add": {
      "description": "Run prettier as an eslint rule.",
      "factory": "./nest-prettier/index#nestPrettier",
      "schema": "./nest-prettier/schema.json"
    }
  }
}

Factory method is nestPrettier in index.ts and schema.json contains a list of questions that prompts users to answer. When users answer them, properties are updated in the schematic context

// schema.ts 

export interface Schema {
  printWidth: number
  tabWidth: number
  useTabs: boolean
  semi: boolean
  singleQuote: boolean
  trailComma: string
  bracketSpacing: boolean
  eslintFileFormat: string
  quoteProps: string
  arrowParens: string
  requirePragma: boolean
  insertPragma: boolean
}

Define the build process of the schematics

When authoring nestjs library, we intend to build and publish the library to npmjs.com for other developers to use. First, we configure tsconfig.json and add new npm scripts in package.json.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "rootDir": "schematics",
    "outDir": "dist",
    ...
  },
  "include": ["schematics/**/*"],
  "exclude": ["schematics/*/files/**/*"]
}

Second, We change “rootDir” from “src” to “schematics” and add "outDir": "dist" to build the schematic in dist/ directory. We modify include array to include files in schematics/ and excludes templates in files/ from compilation.

Then, we tell schematics where to find the collection.json in package.json.

"schematics": "./dist/collection.json",

nest-prettier is a dev dependency in nestjs application

"nest-add": {
  "save": "devDependencies"
},

Next, add prebuild, copy:collection, copy:schematics, postbuild and manual-test scripts in package.json

"scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc -p tsconfig.json",
    "copy:collection": "cpx schematics/collection.json dist && cpx 'schematics/**/schema.json' dist",
    "copy:schematics": "cpx 'schematics/**/files/**' dist",
    "postbuild": "npm run copy:collection && npm run copy:schematics",
    "manual-test": "npm run build && schematics .:nest-add"
}

It is important to copy collection.json, schematic.json and templates in post build stage.

Finally, manual-test script permits us to test the schematic in debug mode without generating new output file.

Create factory method of the schematic

// nest-prettier/index.ts

export function nestPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask())

    addDependencies(tree, context, [prettier, eslintConfigPrettier, eslintPluginPrettier])
    return chain([addPrettierConfig(options), addEsnlintPrettier(options)])
  }
}

Let’s outline the main flow of the schematic

  • Add NodePackageInstallTask to install the library that is mandatory
  • Update package.json of nestjs application to install prettier, eslint-config-prettier and eslint-plugin-prettier dev dendencies
  • Create a single rule that updates .prettierrc and generates the eslint prettier rule from eslintrc-prettier.template template

Implement new schematic rules

In this simple nestjs library, there are 2 rules:

addPrettierConfig – this rule overwrites .prettierrc with user inputs

addEslintPrettier – this rule generates the eslint prettier rule in eslintrc-prettier.template that we have to manually copy to .eslintrc.js. It is because schematics does not have API that can update javaScript file easily.

Overwrite Prettier configuration file

export function addPrettierConfig(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const prettierOptions: Record<string, string | number | boolean> = {
      singleQuote: options.singleQuote,
      trailingComma: options.trailComma,
      tabWidth: options.tabWidth,
      semi: options.semi,
      printWidth: options.printWidth,
      bracketSpacing: options.bracketSpacing,
      useTabs: options.useTabs,
      arrowParens: options.arrowParens,
      quoteProps: options.quoteProps,
      requirePragma: options.requirePragma,
      insertPragma: options.insertPragma,
    }

    const configFileName = '.prettierrc'
    if (tree.exists(configFileName)) {
      tree.overwrite(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`${configFileName} is overwritten`)
    } else {
      tree.create(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`Created ${configFileName}`)
    }
    return tree
  }
}

The code loads the options from Schema interface and invokes Tree API to overwrite .prettierrc with the new format options.

Copy eslint prettier rule to eslint configuration file configuration file

First, we create a template, eslint-prettier.template in nest-prettier/files directory.

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': <%= singleQuote %>, 
        'trailingComma': '<%= trailComma %>', 
        'tabWidth': <%= tabWidth %>, 
        'semi': <%= semi %>, 
        'printWidth': <%= printWidth %>, 
        'bracketSpacing': <%= bracketSpacing %>, 
        'useTabs': <%= useTabs %>,
        'arrowParens': '<%= arrowParens %>',
        'quoteProps': '<%= quoteProps %>',
        'requirePragma': <%= requirePragma %>,
        'insertPragma': <%= insertPragma %>, 
    }]
}

<% singleQuote %> is a parameterized variable that will be replaced by singleQuote property of Schema interface. It is also true to the rest of the parameterized variables.

// eslint-prettier.helper.ts

import { apply, Rule, SchematicContext, template, Tree, mergeWith, url } from '@angular-devkit/schematics'
import { strings } from '@angular-devkit/core'

export function addEslintPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const configFileName = '.eslintrc.js'
    const buffer = tree.read(configFileName)
    if (!buffer) {
      return tree
    }

    ...
    const eslintTemplate = './eslintrc-prettier.template'
    if (tree.exists(eslintTemplate)) {
       tree.delete(eslintTemplate)
    }

    const sourceTemplate = url('./files')
    const sourceParameterizedTemplate = apply(sourceTemplate, [
        template({
          ...options,
          ...strings,
        }),
    ])
    
    return mergeWith(sourceParameterizedTemplate)(tree, context)
  }
}

url('/files') reads all the files in nest-prettier/files directory; apply replaces variables with option values and mergeWith adds the templates to the source tree of the nestjs application.

As the result, the rules have set up to achieve file format in prettier and eslint configuration files. Before publishing the library, we should test the schematic in debug mode and/or in a local nestjs application

Test in debug mode after authoring a nestjs library

npm run manual-test  

compiles the codes to dist/ directory and executes schematics .:nest-add.

We choose our prettier configuration in debug mode. Suppose the schematic is correct, the outcome is to overwrite .prettierrc and create eslintrc-prettier.template.

Debug mode enabled by default for local collections.
? Eslint file format. javascript
? What is the print width per line? 120
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the 
file. This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
Dry run enabled by default in debug mode. No files written to disk.

Test in nestjs application after authoring a nestjs library

Debug mode does not create anything; therefore, I suggest to test the library in a local nestjs application.

First, we run npm link command to link the nest-prettier dependency to nest-prettier library

npm link ../nest-prettier

Then, execute nest-add schematic of the nest-prettier library

schematics nest-prettier:nest-add
? Eslint file format. javascript
? What is the print width per line? 140
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file.
 This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
✔ Packages installed successfully.

We verify that schematic has replaced the variables of .eslintrc-prettier.template with option values

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': false, 
        'trailingComma': 'all', 
        'tabWidth': 2, 
        'semi': true, 
        'printWidth': 140, 
        'bracketSpacing': true, 
        'useTabs': false,
        'arrowParens': 'always',
        'quoteProps': 'as-needed',
        'requirePragma': false,
        'insertPragma': false, 
    }]
}

The manual work is to copy “prettier/prettier” and prettier plugin to .eslintrc.js

// .eslintrc.js

extends: [
  'plugin:@typescript-eslint/recommended',
  'plugin:prettier/recommended'
],

rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    'prettier/prettier': ['error', {
      'singleQuote': false, 
      'trailingComma': 'all', 
      'tabWidth': 2, 
      'semi': true, 
      'printWidth': 140, 
      'bracketSpacing': true, 
      'useTabs': false,
      'arrowParens': 'always',
      'quoteProps': 'as-needed',
      'requirePragma': false,
      'insertPragma': false, 
    }]
},

Publish the nestjs library

My experience is to publish the library on the command line. We need to create an account in npmjs.com and enable 2FA to secure the account.

Subsequently, go back to the command line to add user on the machine

npm adduser

npm notice Log in on https://registry.npmjs.org/
Username:
Password: 
Email: (this IS public)

Then, publish the library and input OTP to deploy to npmjs.com

npm publish
Enter OTP:

Final thoughts

In this post, we have seen that writing nestjs library is made possible by Angular schematics. After testing the library locally, we can publish it to npmjs.com, execute nest add command to install the library and generates code in nestjs application

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. nest-prettier library: https://www.npmjs.com/package/nest-prettier
  2. Generating code using schematics: https://angular.io/guide/schematics

Swagger In NestJS

Reading Time: 5 minutes

 192 total views,  4 views today

Problem: Frontend developers complained that integration with API is a painful process because there is no documentation on available endpoints, expected payloads and responses. Therefore, our backend team leads elect to enable swagger such that frontend developers can browse all the APIs on dedicated documentation site.

When they showed me how it was done, I was thoroughly impressed and wanted to show NestJS peers the simple steps.

Create a NestJS application

nest new nest-swagger

Install configuration, swagger and other dependencies

npm install --save @nestjs/config joi class-transformer class-validator
npm install --save @nestjs/swagger swagger-ui-express

Store environment variables in environment file

Store Swagger environment variables in .env and we will use configuration service to retrieve the values in main.ts

// main.ts
NODE_ENV=development
PORT=3000
API_VERSION=1.0
SWAGGER_TITLE=Local Swagger Documentation Site
SWAGGER_DESCRIPITON=The task API description

Import Configuration Module and validate schema

// envSchema.ts
import * as Joi from 'joi'

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').default('development'),
  PORT: Joi.number().default(3000),
  API_VERSION: Joi.string().default('1.0'),
  SWAGGER_TITLE: Joi.string().required(),
  SWAGGER_DESCRIPITON: Joi.string().required(),
})
// app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { validationSchema } from './envSchema'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Configure Swagger

Initialize Swagger in main.ts such that API documentation is generated automatically for controllers, entities and data transfer objects (DTOs).

// main.ts
async function bootstrap() {
   ....
   app.useGlobalPipes(
     new ValidationPipe({
       whitelist: true,
     }),
   )

   const configService = app.get(ConfigService)
   const version = configService.get<string>('API_VERSION', '1.0')
   const title = configService.get<string>('SWAGGER_TITLE', '')
   const description = configService.get<string>('SWAGGER_DESCRIPITON', '')

   const config = new DocumentBuilder()
     .setTitle(title)
     .setDescription(description)
     .setVersion(version)
     .addBearerAuth()
     .build()
   const document = SwaggerModule.createDocument(app, config)
   SwaggerModule.setup('api', app, document)

   const port = configService.get<number>('PORT', 0)
   await app.listen(port)
}

Enable Swagger plugin in nest-cli.json

// nest-clis.json
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}

After Swagger plugin is enabled, Swagger automatically generates documentation for controller and files with either .entity.s or .dto.ts suffix.

Verify Swagger is working by browsing http://localhost:3000/api

The documentation site works because it publishes the GET endpoint of app controller successfully.

Swagger setup is complete and we can add new API and publish the new endpoints in the dedicated documentation site.

Create a new Task Module to generate API documentation

nest g mo Task

Next, I generate task service and task controller and register them in the TaskModule. The following is the declaration of the TaskModule:

import { Module } from '@nestjs/common'
import { TaskController } from './task.controller'
import { TaskService } from './services'

@Module({
  controllers: [TaskController],
  providers: [TaskService],
})
export class TaskModule {}

Define new entities and new DTOs for Task API

// create-task.dto.ts
import { IsNotEmpty, IsString } from 'class-validator'

export class CreateTaskDto {
  @IsNotEmpty()
  @IsString()
  name: string
}

// update-task.dto.ts
import { IsBoolean, IsDefined } from 'class-validator'

export class UpdateTaskDto {
  @IsDefined()
  @IsBoolean()
  completed: boolean
}

Define new entities for the Task API

// task.entity.ts
import { IsBoolean, IsString } from 'class-validator'

export class Task {
  @IsString()
  id: string

  @IsString()
  name: string

  @IsBoolean()
  completed: boolean
}

// delete-result.entity.ts
import { IsNumber, IsOptional, Min, ValidateNested } from 'class-validator'
import { Task } from './task.entity'

export class DeleteResult {
  @IsNumber()
  @Min(0)
  count: number

  @IsOptional()
  @ValidateNested()
  task?: Task
}

Create CRUD methods in Task Service

Create basic methods to add, update, delete and retrieve a task object from a tasks array

// task.service.ts
import { BadRequestException, Injectable } from '@nestjs/common'
import { v4 } from 'uuid'
import { CreateTaskDto, UpdateTaskDto } from '../dtos'
import { DeleteResult, Task } from '../entities'

@Injectable()
export class TaskService {
  tasks: Task[] = [
    ...some tasks objects
  ]

  listAll(): Promise<Task[]> {
    return Promise.resolve(this.tasks)
  }

  findById(id: string): Promise<Task> {
    const task = this.tasks.find((task) => task.id === id)
    if (!task) {
      throw new BadRequestException('Task not found')
    }
    return Promise.resolve(task)
  }

  deleteById(id: string): Promise<DeleteResult> {
    const task = this.tasks.find((task) => task.id === id)
    this.tasks = this.tasks.filter((task) => task.id !== id)
    const deletedResult: DeleteResult = {
      count: task ? 1 : 0,
      task,
    }
    return Promise.resolve(deletedResult)
  }

  updateTask(id: string, dto: UpdateTaskDto): Promise<Task> {
    this.tasks = this.tasks.map((task) => {
      if (task.id !== id) {
        return task
      }
      return {
        ...task,
        completed: dto.completed,
      }
    })

    return this.findById(id)
  }

  createTask(dto: CreateTaskDto): Promise<Task> {
    const newTask: Task = {
      ...dto,
      id: v4(),
      completed: false,
    }
    this.tasks = [...this.tasks, newTask]
    return Promise.resolve(newTask)
  }
}

Create endpoints in Task Controller

task.controller.ts
import { UpdateTaskDto } from './dtos/update-task.dto'
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put } from '@nestjs/common'
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger'
import { CreateTaskDto } from './dtos'
import { TaskService } from './services'
import { DeleteResult, Task } from './entities'

@ApiTags('Task')
@Controller('task')
export class TaskController {
  constructor(private service: TaskService) {}

  @Get()
  getAll(): Promise<Task[]> {
    return this.service.listAll()
  }

  @Get(':id')
  @ApiBadRequestResponse({ description: 'Task not found' })
  getTask(@Param('id', ParseUUIDPipe) id: string): Promise<Task> {
    return this.service.findById(id)
  }

  @Delete(':id')
  deleteTask(@Param('id', ParseUUIDPipe) id: string): Promise<DeleteResult> {
    return this.service.deleteById(id)
  }

  @Post()
  createTask(@Body() dto: CreateTaskDto): Promise<Task> {
    return this.service.createTask(dto)
  }

  @Put(':id')
  updateTask(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateTaskDto): Promise<Task> {
    return this.service.updateTask(id, dto)
  }
}

@ApiBadRequestResponse and @ApiBadRequestResponse are Swagger decorators.

DecoratorDescription
ApiTagsProvide name of the API and in this example, the name is Task
ApiBadRequestResponseIndicate the endpoint returns BadRequestException when task is not found

Refresh http://localhost:3000/api and Task API is published.

Task lists all available endpoints (two GETs, POST, DELETE and POST) and Schema section auto-generates its entities (Task and DeleteResult) and DTOs (CreateTaskDto and UpdateTaskDto).

That’s the end of Swagger introduction.

Final thoughts

Swagger provides clear documentation of Restful API that facilitates communication between frontend and backend developers. Rather than constantly asking backend team for sample requests and responses, frontend developers can refer to the Swagger documentation that illustrates all available endpoints and the expected payloads and responses. When teams have no confusion and argument, developers become happy and happy developers are the most productive in application development.

I hope you like this article and share it to people who are interested in NestJS!

Resources:

  1. Repo: https://github.com/railsstudent/nest-swagger
  2. OpenAPI (Swagger): https://docs.nestjs.com/openapi/introduction

Dynamic Task Scheduler In NestJS

Reading Time: 7 minutes

 182 total views,  2 views today

Scenario: Our ESG (Environmental, Social, and Governance) platform requires to run schedule jobs to call endpoints to generate reports for internal teams and send company emails to our customers. Development team was tasked with developing a job scheduler that executes cron jobs at a specified period of time. node-cron is a popular open source library for setting up node-like cron jobs in a typescript program, however, Nest provides Schedule module that does basically the same thing with decorators and flexible API. Since our backend is powered by Nest, we prefer to use existing features of NestJS instead of bringing in a new library and investing time to study a new API.

Problem: We followed Nest documentation and created all cron jobs declaratively. These jobs run on a daily or weekly basis that are inconvenient for QA team to test because they cannot wait for such a long time to verify test results. The approach is to configure jobs to run in different intervals in different environments. For example, a job is configured to run once a day in production environment and run every minute in QA environment to facilitate testing cycle.

To cater to the new QA request, developers converted declarative cron jobs to dynamic counterparts through dynamic schedule module API. However, the codes were no longer DRY and I refactored the codebase to keep future development efforts to the minimum.

Create a NestJS application

nest new nest-dynamic-scheduler

Install configuration, http and schedule dependencies

npm i --save @nestjs/config joi
npm i --save @nestjs/axios
npm install --save @nestjs/schedule
npm install --save-dev @types/cron

Store environment variables in environment file

Store environment variables in .env and we will use configuration service to retrieve the base url in the codes.

Import Configuration Module

Import configuration module and validate the schema of .env file.

Validation part completes. We can move on to add dummy endpoints in AppController and a Task module that calls them in cron jobs.

Create dummy endpoints in App Controller

We define a printMessage function in AppService that will be called by the AppController

AppController has three new endpoints: the first one handles POST method, the second one handles PATCH and the final one handles PUT. It is to demonstrate that Nest cron jobs work with common http request methods.

Create new Task module

nest g mo Task

Next, I generate task service and import HttpModule and ScheduleModule.forRoot() to TaskModule. The following is the declaration of the TaskModule:

import { HttpModule, Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'
import { TaskService } from './task.service'

@Module({
  imports: [HttpModule, ScheduleModule.forRoot()],
  providers: [TaskService],
  controllers: [],
})
export class TaskModule {}

First Attempt: Create declarative cron jobs in TaskService

// task.service.ts

private readonly baseUrl: string

constructor(
    private httpService: HttpService,
    private configService: ConfigService,
  ) {
    this.baseUrl = this.configService.get<string>('BASE_URL', '')
}

@Cron('*/15 * * * * *')
async handlePostJob(): Promise<void> {
    try {
      await this.httpService
        .post(`${this.baseUrl}/post-job`, {
          name: 'connie',
          msg: 'schedule post job every 15 second',
          timestamp: Date.now(),
        })
        .toPromise()
   } catch (err) {
      console.error(err)
   }
}

@Cron('*/20 * * * * *')
async handlePatchJob(): Promise<void> {
  try {
    await this.httpService
       .patch(`${this.baseUrl}/patch-job`, {
          name: 'connie',
          msg: 'schedule patch job every 20 second',
          timestamp: Date.now(),
       })
       .toPromise()
  } catch (err) {
     console.error(err)
  }
}

@Cron('*/30 * * * * *')
async handlePutJob(): Promise<void> {
  try {
    await this.httpService
      .put(`${this.baseUrl}/put-job`, {
         name: 'connie',
         msg: 'schedule put job every 30 second',
         timestamp: Date.now(),
      })
      .toPromise()
  } catch (err) {
    console.error(err)
  }
}

@Cron() decorator tells NestJS when the job starts and terminates and its frequency. The decorator supports all standard cron patterns; therefore, @Cron(‘*/15 * * * * *’) makes the Http POST request every 15 seconds. Similarly, PATCH and PUT requests are triggered every 20 and 30 seconds respectively.

Second Attempt: Convert declarative cron jobs to dynamic cron jobs

If QA team did not make the request, the scheduler was completed and we could go home and rest. The request seemed trivial; we could store the cron patterns as environment variables and applied them in @Cron(), right? Wrong answer, my friends.

// task.service.ts

const defaultInterval = '* * * * * *'    
this.postJobInterval = this.configService.get<string>('POST_JOB_INTERVAL', defaultInterval)

@Cron(this.postJobInterval)
async handlePostJob(): Promise<void> {
    ...
}

I couldn’t use this.postJobInterval inside @Cron() because “this” variable can be null. Fortunately, dynamic scheduler module API exists and we can add cron jobs programmatically with configurable cron patterns.

We define new job intervals in .env (POST_JOB_INTERVAL, PATCH_JOB_INTERVAL, PUT_JOB_INTERVAL)

.env

POST_JOB_INTERVAL='*/15 * * * * *'
PATCH_JOB_INTERVAL='*/20 * * * * *'
PUT_JOB_INTERVAL='*/30 * * * * *'

We comment out the old codes and define addCronJobs function to create dynamic cron jobs and register them in scheduler registry. Finally, we start the jobs such that they are triggered every 15, 20 and 30 seconds. Remember to inject SchedulerRegistry in the constructor.

// task.service.ts

const defaultInterval = '* * * * * *'    

private readonly postJobInterval: string
private readonly patchJobInterval: string
private readonly putJobInterval: string
private readonly baseUrl: string

constructor(
    private httpService: HttpService,
    private schedulerRegistry: SchedulerRegistry,
    private configService: ConfigService,
  ) {
    this.postJobInterval = this.configService.get<string>('POST_JOB_INTERVAL', defaultInterval)
    this.patchJobInterval = this.configService.get<string>('PATCH_JOB_INTERVAL', defaultInterval)
    this.putJobInterval = this.configService.get<string>('PUT_JOB_INTERVAL', defaultInterval)
    this.baseUrl = this.configService.get<string>('BASE_URL', '')
}

addCronJobs(): void {
    const postJob = new CronJob(this.postJobInterval, async () => {
      try {
        await this.httpService
          .post(`${this.baseUrl}/post-job`, {
            name: 'connie',
            msg: 'schedule post job every 15 second',
            timestamp: Date.now(),
          })
          .toPromise()
      } catch (err) {
        console.error(err)
      }
    })

    const patchJob = new CronJob(this.patchJobInterval, async () => {
      try {
        await this.httpService
          .patch(`${this.baseUrl}/patch-job`, {
            name: 'connie',
            msg: 'schedule patch job every 20 second',
            timestamp: Date.now(),
          })
          .toPromise()
      } catch (err) {
        console.error(err)
      }
    })

    const putJob = new CronJob(this.putJobInterval, async () => {
      try {
        await this.httpService
          .put(`${this.baseUrl}/put-job`, {
            name: 'connie',
            msg: 'schedule put job every 30 second',
            timestamp: Date.now(),
          })
          .toPromise()
      } catch (err) {
        console.error(err)
      }
    })

    this.schedulerRegistry.addCronJob('post-job', postJob)
    this.schedulerRegistry.addCronJob('patch-job', patchJob)
    this.schedulerRegistry.addCronJob('put-job', putJob)
    postJob.start()
    patchJob.start()
    putJob.start()
  }

Modify TaskModule to implement OnModuleInit interface and start cron jobs in onModuleInit function.

// task.module.ts

export class TaskModule implements OnModuleInit {  
  constructor(private taskService: TaskService) {}
  async onModuleInit() {    
    await taskService.addCronJobs()    
  }
}

Start the application and observe the messages in the terminal

npm run start:dev

The current implementation meets the requirements but it is not DRY. Why do I say that? It is because I see a common pattern in the three dynamic jobs.

  • Create a callback function that
    • makes a new HTTP request in a try-catch block
    • converts the observable to promise with toPromise()
    • await the result of the HTTP request
  • Create a new cron job with cron pattern and the callback function
  • Add the new cron job to scheduler registry
  • Start the cron job

I can generalize this pattern such that developer can write minimum codes to add new cron job in the future.

Third Attempt: Generalize the dynamic task scheduler

Define a new interface, JobConfiguration, that stores the metadata of the job

// job-configuration.interface.ts
import { Method } from 'axios'

export interface JobConfiguration {
  url: string
  interval: string
  method: Method
  dataFn: () => any
  name: string
}

method is the supported method of Axios and it can be POST, PATCH, PUT or DELETE. dataFn function is used to generate new request data in the callback.

Define metadata of cron jobs in jobConfigurations array.

this.jobConfigurations = [
      {
        url: `${this.baseUrl}/post-job`,
        interval: this.postJobInterval,
        method: 'POST',
        dataFn: () => ({
          name: 'connie',
          msg: 'schedule dynamic post job every 15 second',
          timestamp: Date.now(),
        }),
        name: 'post-job2',
      },
      {
        url: `${this.baseUrl}/patch-job`,
        interval: this.patchJobInterval,
        method: 'PATCH',
        dataFn: () => ({
          name: 'mary',
          msg: 'schedule dynamic patch job every 20 second',
          timestamp: Date.now(),
        }),
        name: 'patch-job2',
      },
      {
        url: `${this.baseUrl}/put-job`,
        interval: this.putJobInterval,
        method: 'PUT',
        dataFn: () => ({
          name: 'job',
          msg: 'schedule dynamic put job every 30 second',
          timestamp: Date.now(),
        }),
        name: 'put-job2',
      },
]

Define a high-order function (HOF), callbackGenerator, that returns a callback function. The callback function is designed to construct Http request to perform a task.

private callbackGenerator(configuration: JobConfiguration): () => Promise<void> {
    const { url, method, dataFn } = configuration
    return async () => {
      try {
        await this.httpService
          .request({
            url,
            method,
            data: dataFn(),
          })
          .toPromise()
      } catch (err) {
        console.error(err)
      }
    }
 }

Lastly, define addConfigurableCronJobs function to iterate jobConfigurations array and insert the cron jobs to scheduler registry

addConfigurableCronJobs(): void {
    for (const configuration of this.jobConfigurations) {
      const { interval, name } = configuration
      const callback = this.callbackGenerator(configuration)
      const cronjob = new CronJob(interval, callback)
      this.schedulerRegistry.addCronJob(name, cronjob)
      cronjob.start()
    }
}

In task.module.ts,  update onModuleInit function to call addConfigurableCronJobs instead and implement onModuleDestroy to stop cron jobs when application shutdown. 

// task.module.ts
export class TaskModule implements OnModuleInit {  
  constructor(private taskService: TaskService) {}
  async onModuleInit() {    
     // await taskService.addCronJobs()    
     await this.taskService.addConfigurableCronJobs()  
  }
  
  async onModuleDestroy() {    
     await this.taskService.deleteConfigurableCronJobs()  
  }
}
// task.sevice.ts
export class TaskService {  
  ...
  
  deleteConfigurableCronJobs(): void {    
     for (const configuration of this.jobConfigurations) {      
        const { name } = configuration      
        this.schedulerRegistry.deleteCronJob(name)    
     }  
   }
}

deleteConfigurableCronJobs function iterates jobConfigurations array and deletes the jobs one by one.

In main.ts,  add app.enableShutdownHooks() to listen to application shutdown event that ultimately calls onModuleDestroy to stop the cron jobs.

// main.ts
async function bootstrap() {
  ...
  
  // Starts listening for shutdown hooks  
  app.enableShutdownHooks()
  const configService = app.get(ConfigService)
  const port = configService.get<number>('PORT', 0)
  await app.listen(port)
}

Start the application and observe the messages in the terminal

npm run start:dev

Similar to task scheduler v2, cron jobs are triggered every 15, 20 and 35 seconds. We have completed the refactoring and scheduler codes are DRY.

Add New Cron Job

It is made easy after the refactoring. Developers simply add new job configuration in the jobConfigurations array and addConfigurableCronJobs takes care of callback creation, job registration and job launch automatically. Less code and less development time on the part of developers.

Final thoughts

This post shows how our company implements dynamic task scheduler using HttpModule and ScheduleModule of NestJS. We did not settle for the non-DRY version after fulfilling QA request. We identified common pattern and generalized the architecture with high-order function and metadata array. The outcome is elimination of duplicated codes and easy code changes to support new cron job.

I hope you like this article and share it to people who are interested in NestJS!

Resources:

  1. Repo: https://github.com/railsstudent/nest-dynamic-scheduler
  2. Nest Task Scheduling: https://docs.nestjs.com/techniq\ues/task-scheduling
  3. Nest Http Module: https://docs.nestjs.com/techniques/http-module

Stripe Integration In NestJS

Reading Time: 6 minutes

 164 total views,  2 views today

Scenario: Our ESG (Environmental, Social, and Governance) platform offers monthly and annual subscriptions to charge customers for using our ESG reporting service. When we designed our payment module, we chose Stripe as our payment platform because it accepts credit card payment method, provides good documentations and open source node library for the API (stripe-node).

Problem: We have chosen our payment platform but we don’t want the front-end application to call Stripe API directly. The architecture design is to make HTTP requests to NestJS backend and the backend calls node Stripe library to update Stripe accounts and our database. It is a challenge to our development team because we have never done Stripe integration in NestJS before and we intend to implement it by adopting good NestJS philosophy.

Go to Stripe.com to sign up a developer account

Copy the secret key as it will be passed to Stripe API to update Stripe accounts

Create a NestJS application

nest new nest-stripe-integration

Install Stripe and other dependencies

npm i stripe --save
npm i --save @nestjs/config joi

Store Stripe secret key in environment file

Store environment variables in .env and we will use configuration service to retrieve the secret key in the codes.

Import Configuration Module

Import configuration module and validate the schema of .env file.

Validation part completes and we can move on to create a Stripe module that is the core of this post.

Create a Stripe module to encapsulate node Stripe library

nest g mo Stripe

Next, I run nest commands to generate stripe service and stripe controller and the following is the declaration of the StripeModule:

@Module({
  providers: [StripeService],
  controllers: [StripeController],
})
export class StripeModule {}

Add logic to Stripe service to create and retrieve customer

The create customer logic is going to create a new credit card and assign it to the new customer. For brevity’s sake, I won’t show the implementation of createCard here and you can visit my github repo to find it.

// stripe.service.ts

import Stripe from 'stripe'

constructor(private service: ConfigService) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }
     
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })    

    const { data } = await stripe.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await stripe.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}

The get customer logic accepts Stripe customer id and returns the Stripe customer object if it exists

async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })

    const customer = await stripe.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
  }

We have completed the Stripe service and can proceed to add endpoints to the controller.

Add new endpoints to the controller

@Controller('stripe')
export class StripeController {
  constructor(private service: StripeService) {}

  @Post('customer')
  async createCustomer(@Body() dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    return this.service.createCustomer(dto)
  }

  @Get('customer/:customerId')
  async getCustomer(@Param('customerId') customerId: string): Promise<Stripe.Customer | null> {
    return this.service.getCustomer(customerId)
  }
}

The controller is also completed and we can get ourselves new customers.

CURL

// Create new customer
curl --location --request POST 'http://localhost:3000/stripe/customer' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "John Doe",
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "email": "john.doe@yopmail.com",
    "card": {
        "name": "John Doe",
        "number": "4242424242424242",
        "expMonth": "01",
        "expYear": "2026",
        "cvc": "315" 
    }
}

// Response
{
    "id": "cus_Jx6caVHGwhTrAK",
    "object": "customer",
    "address": null,
    "balance": 0,
    "created": 1627715672,
    "currency": null,
    "default_source": null,
    "delinquent": false,
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "discount": null,
    "email": "john.doe@yopmail.com",
    "invoice_prefix": "4D7BFE8B",
    "invoice_settings": {
        "custom_fields": null,
        "default_payment_method": null,
        "footer": null
    },
    "livemode": false,
    "metadata": {},
    "name": "John Doe",
    "next_invoice_sequence": 1,
    "phone": null,
    "preferred_locales": [],
    "shipping": null,
    "tax_exempt": "none"
}
// Retrieve customer
curl --location --request GET 'http://localhost:3000/stripe/customer/cus_Jx6caVHGwhTrAK'

// Response
{
    "id": "cus_Jx6caVHGwhTrAK",
    "object": "customer",
    "address": null,
    "balance": 0,
    "created": 1627715672,
    "currency": null,
    "default_source": "card_1JJCOzBX6xHuypyKQ5nw4PxY",
    "delinquent": false,
    "description": "Stripe Customer (john.doe@yopmail.com)",
    "discount": null,
    "email": "john.doe@yopmail.com",
    "invoice_prefix": "4D7BFE8B",
    "invoice_settings": {
        "custom_fields": null,
        "default_payment_method": null,
        "footer": null
    },
    "livemode": false,
    "metadata": {},
    "name": "John Doe",
    "next_invoice_sequence": 1,
    "phone": null,
    "preferred_locales": [],
    "shipping": null,
    "tax_exempt": "none"
}

At this point, Stripe module has successfully provided Stripe functionality but we can make improvements to Stripe service to make it dry

Improvement #1: Make a reusable function to return Node stripe client

If you observe closely, you will see that every function instantiates a node Stripe client to call the library in order to perform customer-related activity.

We can create a small function that returns the client and reuse it in other functions

initStripeClient() {
    const secretKey = this.service.get<string>('STRIPE_SECRET_KEY') || ''
    const stripe = new Stripe(secretKey, {
        apiVersion: '2020-08-27',
    })
    return stripe
}
// stripe.service.ts

import Stripe from 'stripe'

constructor(private service: ConfigService) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }
     
    const stripe = this.initStripeClient()

    const { data } = await stripe.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await stripe.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}
async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const stripe = this.initStripeClient()

    const customer = await stripe.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
 }

We can take one step further by eliminating initStripeClient and injecting the Stripe client in the constructor. How do we do that? The answer is to create a custom provider for the Stripe client in StripeModule.

Improvement #2: Create a custom provider for the Stripe client

// stripe.module.ts

const StripeClientProvider = {
  provide: 'StripeClient',
  inject: [ConfigService],
  useFactory: (service: ConfigService) => {
    const secretKey = service.get<string>('STRIPE_SECRET_KEY', '')
    const stripe = new Stripe(secretKey, {
      apiVersion: '2020-08-27',
    })
    return stripe
  },
}

@Module({
  providers: [StripeService, StripeClientProvider],
  controllers: [StripeController],
})
export class StripeModule {}

Improvement #3: Inject the Stripe client to the constructor of Stripe service

// stripe.service.ts

constructor(@Inject('StripeClient') private stripeClient: Stripe) {}

async createCustomer(dto: CreateCustomerDto): Promise<Stripe.Response<Stripe.Customer>> {
    const { card = null, email, name, description } = dto || {}

    if (!card) {
      throw new BadRequestException('Card information is not found')
    }

    const { data } = await this.stripeClient.customers.list({
      email,
      limit: 1,
    })

    if (data.length > 0) {
      throw new BadRequestException('Customer email is found')
    }

    const newCustomer = await this.stripeClient.customers.create({ email, name, description })
    if (!newCustomer) {
      throw new BadRequestException('Customer is not created')
    }

    await this.createCard(newCustomer.id, card)
    return newCustomer
}

async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
    const customer = await this.stripeClient.customers.retrieve(customerId)

    if (customer.deleted) {
      return null
    }

    const { headers, lastResponse, ...rest } = customer
    return rest as Stripe.Customer
}

This is the final version of stripe service: the functions are DRY and it is possible because of useFactory and injection token provided by NestJs.

Final thoughts

In this post, I have shown the usage of useFactory to create an instance of node-stripe client and make it available in NestJS service through injection. Rather than creating a new stripe client in every method, the service keeps a single reference and uses it to call Stripe APIs to create new Stripe customer. This is an illustration of DRY principle where the methods are lean and without duplicated logic.

I hope you find this blog post helpful and look to add NestJS into your backend technical stack.

Resources:

  1. Repo: https://github.com/railsstudent/nest-stripe-integration
  2. Stripe Token API: https://stripe.com/docs/api/tokens
  3. Stripe Customer API: https://stripe.com/docs/api/customers

How I cache data list in memory using useFactory in NestJS

Reading Time: 3 minutes

 170 total views,  2 views today

Scenario: Application at work stores data lists in database tables and they never change after initial population. These data lists are used for dropdown selection in frontend, for example, users select their salutation in user profile page and click to make the change.

Problem: When user opens the page, UI makes a HTTP request to retrieve the salutation list from the server. NestJS API connects the database to query the table and returns the salutation array in the response. Since the list is static, I want to cache the data in memory such that subsequent database calls can be avoided.

Solution #1: When application starts, store data lists in Redis and the API fetches the data from the in-memory cache.

Cons: This solution will require DevOps engineers to provision resources for the Redis instances in the cloud platform and I cannot justify the cost for setting them up for few data lists.

Solution #2: Use useFactory to create a custom provider to store the data lists in memory and use @Inject() to inject the cache in controller to return the cached data

Pros:

  1. The implementation is done within application level and no DevOps help is required.
  2. If data list changes in the future, TypeORM migration script will be deployed to the environment and application restarts to recreate the cache.

Lets start with the naive approach that queries database upon every http request

Naive Approach

Step 1: Create a service that waits 2 seconds before returning a list of salutations

async getSalutations(): Promise<Salutation> {
    const salutations = ['Mr.', 'Mrs.', 'Miss']
    return new Promise((resolve) => setTimeout(resolve, 2000)).then(() => {
      console.log('Wait 2 seconds before returning salutations array')
      const now = Date.now()
      return {
        now,
        salutations,
      }
    })
  }

The promise returns Data.now() to indicate that the result is obtained at real time.

Step 2: Create an endpoint to return the array of salutations

  @Get('salutations-realtime')
  async getRealtimeSalutations(): Promise<Salutation> {
    return this.appService.getSalutations()
  }

Step 3: Curl http://localhost:3000/salutations-realtime to show the timestamp changes every time

Take note that the timestamp is different each time because each call waits 2 seconds before the array is returned in the response.

Next, I will use useFactory to define a custom provider such that getSalutations() is called only once and the 2-second wait is a fixed cost.

Return data as fast as a cheetah

Custom Provider Approach

Step 1: Define interface of the map of data lists

export interface Salutation {
  now: number
  salutations: string[]
}

export interface DataMap {
  salutations: Salutation
}

Step 2: Add a new function in AppService to return the map of data lists

async getDataMap(): Promise<DataMap> {
   return {
      salutations: await this.getSalutations(),
   }
}

I used await this.getSalutations() to wait for the result and to store it in salutations property of the map.

Step 3: Create a custom provider for the map and register it in AppModule

const DataMapFactory = {
  provide: 'DataMap',
  useFactory: (appService: AppService) => {
    return appService.getDataMap()
  },
  inject: [AppService],
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, DataMapFactory],
  exports: [DataMapFactory],
})
export class AppModule {}

Whenever I inject the provider name ‘DataMap‘, I have access to the map and use ES6 de-structuring to obtain salutations array

Step 4: Inject ‘DataMap’ in AppController and add a new /salutations endpoint to verify that it actually does what I intend

  constructor(private readonly appService: AppService, @Inject('DataMap') private dataMap: DataMap) {}

  @Get('salutations')
  async getSalutations(): Promise<Salutation> {
    const { salutations } = this.dataMap
    return salutations
  }

Last step: CURL /salutations and verify the timestamp does not change

All CURL responses have the same timestamp and Date.now() can only increase; therefore, /salutations endpoint returns the array from the cached map.

Final thoughts

In this post, I have shown one use case of useFactory of NestJS but useFactory can cache any resource, for example, database connection and instances of third-party libraries. In the future, I can write an article on how to create a custom provider for Node Stripe library in NestJS.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version
  2. Custom Provider: https://docs.nestjs.com/fundamentals/custom-providers