Queue and job processing in nestjs

Reading Time: 5 minutes

 26 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

 60 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

 59 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: 2018service.get<number>('YEAR'),
       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

 58 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

 136 total views

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

 140 total views

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

 120 total views

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

 118 total views

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

End to end testing (e2e) in NestJS

Reading Time: 4 minutes

 124 total views

This is part 3 of Use TypeOrm Custom Repository in NestJS and this post is about end- to-end testing (e2e) of NestJS endpoints.

In part one and part two, controllers, services and custom repositories are tested and they pass all the test cases. The final step is to validate HTTP requests between frontend and backend and the simulated application flows should always return the expected responses. This type of testing is called end-to-end testing because the application flow happens from the beginning to the end.

An analogy of e2e testing is my vacation to Canada. Whenever I visit Canada, my father requests to buy canned salmon, my old sister and I respond by driving to Costco warehouse to buy a pack of canned salmon and a few bags of Lays Ketchup chips. They are in my baggage and import to Hong Kong via cargo.

User API

In this post, I am going to do e2e testing on User API . The api is defined in user controller and consisted of four endpoints.

// user.controller.ts

// return all users
GET /user

// return user and the reports created by the user
GET /user/:id/report

// return user matching user id
GET /user/:id

// create new user
POST /user

Similar to unit tests, NestJS uses Jest and Supertest to write e2e test cases and make HTTP requests respectively, and validate status code and response.

Set up 1: Generate constants and mock data

Install uuid library that provides function to generate unique uuid.

npm install uuid
npm install --save @types/uuid
// constant.mock.ts

import { User } from '@/entities'
import { v4 } from 'uuid'

export const now = new Date()
export const userId = v4()
export const userId2 = v4()
export const reportId = v4()
export const newUserId = v4()
// user.mock.ts

import { CreateUserDto } from '@/user'
import { Report, User } from '@/entities'
import { newUserId, now, reportId, userId, userId2 } from './constant.mock'

// For brevity, delete initialization codes

export const allUsers: User[] = [
  {
    id: userId,
    name: 'John',
    lastname: 'Doe',
    age: 10,
    createdAt: now,
    updatedAt: now,
    version: 1,
    reports: [],
  },
  {
    id: userId2,
    name: 'Jane',
    lastname: 'Doe',
    age: 15,
    createdAt: now,
    updatedAt: now,
    version: 1,
    reports: [],
  },
]

export const expectedUser = {
  id: userId,
  name: 'John',
  lastname: 'Doe',
  age: 10,
  createdAt: now.toISOString(),
  updatedAt: now.toISOString(),
  version: 1,
  reports: [],
}

export const userService = {
  getUsers: () => allUsers,
  getUser: (id: string) => allUsers.find((user) => user.id === id),
  createUser: (dto: CreateUserDto) => {
    const newUser = {
      ...dto,
      id: newUserId,
      createdAt: now,
      updatedAt: now,
      version: 1,
      reports: [],
    }
    return newUser
  },
  getUserWithReports: (id: string) => {
    ...
  },
}

Set up 2: Create Testing Module and Initialize Test App

beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [UserService],
    })
      .overrideProvider(UserService)
      .useValue(userService)
      .compile()

    app = moduleFixture.createNestApplication()
    app.useGlobalPipes(new ValidationPipe())
    await app.init()
})
.overrideProvider(UserService).useValue(userService)

replaces the real user service with mocked service

app.useGlobal(new ValidationPipe())

validates data transfer object (DTO) and throws exception when property fails the condition of class-validator decorators.

Example 1: Test cases to validate GET /user/:id

describe('/user/:id (GET)', () => {
   it('should return user', () => {
      return request(app.getHttpServer())
         .get(`/user/${userId}`)
         .expect(HttpStatus.OK)
         .expect(expectedUser)
    })

    it('should return undefined', () => {
      const id = v4()
      return request(app.getHttpServer())
          .get(`/user/${id}`)
          .expect(HttpStatus.OK)
          .expect({})
    })
}

The first test case validates that request(app.getHttpServer()).get(`/user/${userId}`) returns 200 status and calls getUser() to return user with matching user id.

The second test case validates that request(app.getHttpServer()).get(`/user/${userId}`) returns 200 status and calls getUser() to return undefined user.

Example 2: Negative test cases to validate POST /user

CreateUserDto requires non-empty first name, last name and non-negative age. If invalid DTO is sent in the request, the response should return 400 status. Lets write some test cases to validate the response of the requests

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

export class CreateUserDto {
  @IsNotEmpty()
  @IsString()
  name: string

  @IsNotEmpty()
  @IsString()
  lastname: string

  @IsNotEmpty()
  @Min(0)
  @IsNumber()
  age: number
}
describe('/user (POST)', () => {
    it('should throw error when age is negative', () => {
      const dto = {
        name: 'John',
        lastname: 'Doe',
        age: -1,
      }
      return request(app.getHttpServer())
          .post('/user')
          .send(dto)
          .expect(HttpStatus.BAD_REQUEST)
    })

    it('should throw error when name is blank', () => {
      const dto = {
        name: '',
        lastname: 'Doe',
        age: 62,
      }
      return request(app.getHttpServer())
          .post('/user')
          .send(dto)
          .expect(HttpStatus.BAD_REQUEST)
    })

})

Example 3: Positive test case to validate POST /user

describe('/user (POST)', () => {
    it('should create and return new user', () => {
      const dto = {
        name: 'John',
        lastname: 'Doe',
        age: 10,
      }
      return request(app.getHttpServer())
        .post('/user')
        .send(dto)
        .expect(HttpStatus.CREATED)
        .expect({
          ...dto,
          id: newUserId,
          createdAt: now.toISOString(),
          updatedAt: now.toISOString(),
          version: 1,
          reports: [],
        })
    })
})

When DTO is valid, POST /user is successful and it returns 201 status and a new user object. HTTP converts date time values of response to string; therefore, createAt and updatedAt become now.toISOString().

.expect(HttpStatus.CREATED)

returns 201 status

.expect({
    ...dto,
    id: newUserId,
    createdAt: now.toISOString(),
    updatedAt: now.toISOString(),
    version: 1,
    reports: [],
 })

The test case expects createUser() is called to return a new user object; it has new user id and data of DTO.

Final thoughts

E2e testing in NestJS is feasible when you understand the patterns of mocking and testing different HTTP methods (GET, POST, PATCH, DELETE). After grunt work is laid down, e2e test cases are described in several lines of codes. Happy path expects 200/201 status code and an object in response. Fail path expects 400-level or 500-level status code and an error message.

Test automation takes efforts initially but it can be used continuously to catch errors when API changes; therefore, it is a good investment for any developer and development team.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version
  2. https://docs.nestjs.com/fundamentals/testing#end-to-end-testing

Unit Test Custom Repository in NestJS

Reading Time: 5 minutes

 127 total views

This is part 2 of Use TypeOrm Custom Repository in NestJS and this post is about unit testing custom repository.

In part one, I created a NestJS demo with controllers, services and custom repositories. The demo also added unit testing on controllers and services in which I mocked the methods of custom repositories with jest.fn(). The missing piece is unit testing on custom repository and I will fill the hole in this post.

Jest

Jest is an open source JavaScript testing framework that can use with Angular and NestJS. I used Jest to test controllers and services in part 1 and it can also be used to test custom repository.

The functions of the custom repository query database to obtain result set through TypeORM. However, there is no database running in custom repository testing and calling any repository method can lead to error. The solution is to mock the TypeORM functions such that they don’t call the database but the fake implementations to obtain test results. This is where Jest comes in because jest.fn() and jest.Spy().mockImplementation() are designed to do it.

Step 1: Create a spec file for user repository

A spec file is a Javascript/TypeScript file that encapsulates testing codes to validate the functionality of a class. It is important to import the necessary modules and providers to TestingModule before writing the first test case.

// user-repository.spec.ts

describe('UserRepository', () => {
  let service: UserRepository
  let now: Date

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserRepository],
    }).compile()

    service = module.get<UserRepository>(UserRepository)
    now = new Date(Date.now())
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })
})

Step 2: Write test cases to validate basic CRUD functions

Let remind the readers the implementation of UserRepository.

// user.repository.ts

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async getUsers(): Promise<User[]> {
    return this.find({
      order: {
        name: 'ASC',
        lastname: 'ASC',
      },
    })
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.findOne(id)
  }

  async createUser(newUser: NewUser): Promise<User> {
    return this.save(this.create(newUser))
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
  }
}

I consider getUsers, getUser and createUser basic CRUD functions because they are straightforward; calling TypeOrm functions directly to obtain results. getUserWithReports is complicated because it chains builder functions of TypeORM to compose a SQL query to obtain results from the database.

It also means less effort to mock straightforward TypeORM functions than builder functions.

Let’s write test case for getUsers function

describe('getUsers', () => {
    it('should find and return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]
      jest.spyOn(service, 'find').mockImplementation(() => Promise.resolve(users))

      const result = await service.getUsers()
      expect(service.find).toBeCalled()
      expect(service.find).toBeCalledWith({
        order: {
          name: 'ASC',
          lastname: 'ASC',
        },
      })
      expect(result).toEqual(users)
    })
  })

Explanation:

  1. jest.spyOn(service, ‘find’).mockImplement() spies this.find() and provides a mock function that returns a promise that returns User array
  2. const result = await service.getUsers() returns the results of getUsers()
  3. expect(service.find).toBeCalled() ensures that this.find() is called within getUsers()
  4. expect(service.find).toBeCalledWith({ orders: { name: ‘ASC’, lastname: ‘ASC’ }}) ensures that this.find accepts expected argument
  5. expect(result).toEqual(users) ensures that the result is the same as the User array

I am not going to cover getUser() because it is similarly to testing getUsers()

Testing createUser() is similar to testing getUsers() but we have to mock two implementations: this.create and this.save

describe('createUser', () => {
    it('should create a new user', async () => {
      const id = v4()
      const newUser = {
        name: 'New',
        lastname: 'User',
        age: 21,
      }

      const user: User = {
        id,
        name: 'John',
        lastname: 'Doe',
        age: 10,
        createdAt: now,
        updatedAt: now,
        version: 1,
        reports: [],
      }
      jest.spyOn(service, 'create').mockImplementation(() => user)
      jest.spyOn(service, 'save').mockImplementation(() => Promise.resolve(user))

      const result = await service.createUser(newUser)
      expect(service.create).toBeCalled()
      expect(service.save).toBeCalled()
      expect(service.create).toBeCalledWith(newUser)
      expect(service.save).toBeCalledWith(service.create(newUser))
      expect(result).toEqual(user)
    })
})

Explanation:

  1. jest.spyOn(service, ‘create’).mockImplement() spies this.create() and provides a mock function that returns a new user
  2. jest.spyOn(service, ‘save’).mockImplement() spies this.save() and provides a mock function that returns a promise that returns the new user
  3. const result = await service.createUser(newUser) returns the result of createUser()
  4. expect(service.create).toBeCalled() ensures that this.create() is called within createUser()
  5. expect(service.save).toBeCalled() ensures that this.save() is called within createUser()
  6. expect(service.create).toBeCalledWith(newUser) ensures that this.create accepts expected argument
  7. expect(service.save).toBeCalledWith(service.create(newUser)) ensures that this.create is passed to this.save()
  8. expect(result).toEqual(user) ensures that the result is the same as the new user

Step 3: Write test case to validate getUserWithReports

// user.repository.ts

async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
}

Testing getUserWithReports is tricky because the builder functions are chained together and finally getOne() returns an entity to service. Unlike this.find, this.createQueryBuilder does not return entity but returning a class/object with a leftJoinAndSelect function. Similarly, leftJoinAndSelect returns a class/object with a where function and where returns a class/object with getOne function. Once you understand what they do, you can easily mock them with jest.fn().

describe('getUserWithReports', () => {
    it('should return a user with reports', async () => {
      // deleted codes that initialize other objects

      const user: User = {
        ...partialUser,
        reports: [report],
      }

      const getOne = jest.fn(() => user)
      const where = jest.fn(() => ({
        getOne,
      }))
      const leftJoinAndSelect = jest.fn(() => ({
        where,
      }))
      const createQueryBuilder = () =>
        ({
          leftJoinAndSelect,
        } as unknown as SelectQueryBuilder<User>)
      jest.spyOn(service, 'createQueryBuilder').mockImplementation(createQueryBuilder)

      const result = await service.getUserWithReports(id)
      expect(service.createQueryBuilder).toBeCalled()
      expect(service.createQueryBuilder).toBeCalledWith('user')
      expect(leftJoinAndSelect).toBeCalled()
      expect(leftJoinAndSelect).toBeCalledWith('user.reports', 'reports')
      expect(where).toBeCalled()
      expect(where).toBeCalledWith('user.id = :id', { id })
      expect(getOne).toBeCalled()
      expect(result).toEqual(user)
    })
  })

Explanation:

  1. const getOne = jest.fn(() => user) is a mock function that returns a user with reports
  2. const where = jest.fn(() => ({ getOne })) is a mock function that returns the getOne function
  3. const leftJoinAndSelect = jest.fn(() => ({ where })) is a mock function that returns the where function
  4. jest.spyOn(service, ‘createQueryBuilder’).mockImplement(createQueryBuilder) spies this.createQueryBuilder() and provides a mock function that returns the leftJoinAndSelect function
  5. const result = await service.getUserWithReports(id) returns the result of getUserWithReports()
  6. expect(service.createQueryBuilder).toBeCalled() ensures that this.createQueryBuilder() is called
  7. expect(service.createQueryBuilder).toBeCalledWith(‘report’) ensures that this.createQueryBuilder(‘report’) is called
  8. expect(leftJoinAndSelect).toBeCalled() ensures that .leftJoinAndSelect() is called
  9. expect(leftJoinAndSelect).toBeCalledWith(‘user.reports’, ‘reports’) ensures that .leftJoinAndSelect(‘user.reports’, ‘reports’) is called
  10. expect(where).toBeCalled() ensures that .where() is called
  11. expect(where).toBeCalledWith(‘user.id = :id’, { id }) ensures that .where(‘user.id = :id’, { id }) is called
  12. expect(getOne).toBeCalled() ensures that .getOne() is called
  13. expect(result).toEqual(user) ensures that result is the same as user

Final thoughts

Testing is tedious because it involves experience and long hours of practices for developers to mock APIs and functions to allow test cases to perform. Test cases should validate that correct results are received, expected functions are invoked and expected arguments are passed to the functions. Otherwise, developers do not have high confidence that the quality of the software is good before it is shipped to quality assurers for quality assurance.

I hope this article helps developers who use NestJS to build their backend applications and need help to unit test resources (controllers, services and custom repositories).

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version
  2. Clock-In/Out System Part 9: Back-End Testing — Unit Testing of Services