loading...
YggdrasilTS

Create an API service using nestframework

telco2011 profile image David (ダビッド ) ・15 min read

Original Post

In this post, I want to show you how easy is to create a backend API using the nestframework and also, create the swagger documentation.

Table Of Contents

Introduction

I have to say that I am in love with NestJS to build backend applications, services, libraries, etc. using TypeScript. This framework has been the fastest-growing nodejs framework in 2019 due to its awesome features that, besides helps you to build your code, if you are thinking about to build medium or large services and you want it to be maintainable and scalable, also it give you a way to have your project well structured.

Part 1 - Project Creation

API Service

The service that I am going to create is a simple API service that contains 2 endpoints. Each endpoint returns a chart as image in different formats:

  • /image: Return an image as attachment.
  • /image-base64: Return an image as base64 string data.

To build the charts, I am going to use node-echarts library to get ECharts images in the backend world. ECharts is an open-sourced JavaScript visualization tool that in my opinion, it has great options to build tons of different chart types.

You can check its examples if you don't believe me 😛

Let's begging to create.

Requirements

Service Creation

Once all requirements are installed, I am going to use the NestJS CLI to create the project structure:

odin@asgard:~/Blog $ nest new echarts-api-service -p npm

After the execution, I have opened the ~/Blog/echarts-api-service folder in my Visual Studio Code and this is the project structure:

Here, you can see more about the NestJS project structure. I assume that you are familiar with this structure and I continue building the service.

Now, you can run the service using npm start and the service will respond a Hello World! string doing the following request: http://localhost:3000, as you can see in the following image (I'm using REST Client extension for Visual Studio Code).

Before continue, I am going to add a .prettierc file for code formatting using Prettier with the following options:

{
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false,
  "trailingComma": "all",
  "printWidth": 140
}

And also I like to add the hot reload option to check faster my code changes. To do this, and because I am using NestJS CLI, it is only necessary to change the start:dev script inside the package.json as nest start --watch --webpack. In NestJS Hot Reload documentation you can see more options.

Now, I am ready to modify the code to add the above endpoints /image and /image-base64.

'End of Part 1' You can check the project code in the part-1 tag

Part 2 - EchartsService creation

I am going to modify/delete all the not needed files to adapt the project in a better way.

Delete unnecessary files

In my case, It is not needed to use the app.controller.spec.ts. I delete it.

Modifying app.controller.ts file

This file is responsible for handling incoming requests and returning responses to the client and it is the site where I am going to create the endpoints to let NestJS know the code to use when receiving the requests.

import { Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('image')
  async getImage(): Promise<void> {
    console.log('getImage');
  }

  @Post('image-base64')
  async getImageInBase64(): Promise<string> {
    return 'getImageInBase64';
  }
}

More information about controllers in NestJS Controllers documentation.

Once modified, you can start the service again npm run start:dev and execute both requests:

Modifying app.service.ts file

The service is responsible for data storage and retrieval, in my case, the service will be the chart image creator.

Because I use ECharts, with node-echarts, I am going to create a new folder called echarts and inside another folder called entities. After this, move the app.service.ts to this new folder echarts and renamed it to echarts.service.ts.

'WARN' If you use Visual Studio Code, you will see that in every change, the editor adapts the code. Take care if not and adapt the code to be compiled.

After the changes, I am going to create the getImage method which has the code to create the ECharts image. But first, I am going to install the necessary npm dependencies to create the method:

  • npm i --save imagemin imagemin-pngquant imagemin-jpegtran https://github.com/telco2011/node-echarts.git
  • npm i --save-dev @types/imagemin @types/echarts

And the method:

async getImage(opt: Options): Promise<Buffer> {
  return buffer(
    node_echarts({
      option: opt.echartOptions,
      width: opt.options?.width || DEFAULT_IMAGE_WIDTH,
      height: opt.options?.height || DEFAULT_IMAGE_HEIGHT,
    }),
    {
      plugins: [
        imageminJpegtran(),
        imageminPngquant({
          quality: [0.6, 0.8],
        }),
      ],
    },
  );
}

Once all the changes are done, your code won't compile due to the compiler does not find the Options name. This name will be a new class with two options, one to store the ECharts options to create the chart and other to store some properties to create the image that will contain the ECharts.

To do so, I am going to create the next 3 files:

  • src/echarts/entities/options.entity.ts
import { EChartOption } from 'echarts';

import { ImageOptions } from './image-options.entity';

/**
 * Class to configure echarts options.
 */
export class Options {
  echartOptions: EChartOption;

  options?: ImageOptions;
}
  • src/echarts/entities/image-options.entity.ts
/**
 * Class to configure image options.
 */
export class ImageOptions {
  // Image width
  width?: number;

  // Image height
  height?: number;

  // Download file name
  filename?: string;
}

'INFO' The file name structure is important in NestJS because it will be relevant for Part 4 of this post. It is IMPORTANT that the entity file name follows the next structure: [NAME].entity.ts

  • src/echarts/constants.ts
/**
 * Application constants.
 */
export const DEFAULT_IMAGE_WIDTH = 600;

export const DEFAULT_IMAGE_HEIGHT = 250;

export const DEFAULT_FILENAME = 'echarts.png';

Finally the echarts.service.ts code is the following:

import { Injectable } from '@nestjs/common';

import * as node_echarts from 'node-echarts';

import { buffer } from 'imagemin';
import imageminPngquant from 'imagemin-pngquant';
import * as imageminJpegtran from 'imagemin-jpegtran';

import { DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT } from './constants';

import { Options } from './entities/options.entity';

@Injectable()
export class EchartsService {
  /**
   * Get the echarts as image.
   *
   * @param {Options} opt {@link Options}.
   */
  async getImage(opt: Options): Promise<Buffer> {
    return buffer(
      node_echarts({
        option: opt.echartOptions,
        width: opt.options?.width || DEFAULT_IMAGE_WIDTH,
        height: opt.options?.height || DEFAULT_IMAGE_HEIGHT,
      }),
      {
        // plugins to compress the image to be sent
        plugins: [
          imageminJpegtran(),
          imageminPngquant({
            quality: [0.6, 0.8],
          }),
        ],
      },
    );
  }
}

After these updates, the code compiles well and you can start the server again npm run start:dev.

'HINT' If you want, you can add/modify/delete all of these files without stopping the service and you will see how the Hot Reload feature works.

Now, I am going to connect the controller to the service to get all the functionality on. To do this, I am going to make some modifications in the app.controller.ts file to call the getImage method inside the service:

import { Controller, Post, Header, Body, Res } from '@nestjs/common';
import { Response } from 'express';

import { HttpHeaders, MimeType, BufferUtils } from '@yggdrasilts/volundr';

import { EchartsService } from './echarts/echarts.service';
import { Options } from './echarts/entities/options.entity';
import { DEFAULT_FILENAME } from './echarts/constants';

@Controller()
export class AppController {
  constructor(private readonly echartsService: EchartsService) {}

  @Post('image')
  @Header(HttpHeaders.CONTENT_TYPE, MimeType.IMAGE.PNG)
  async getImage(@Body() opt: Options, @Res() response: Response): Promise<void> {
    const result = await this.echartsService.getImage(opt);
    response.setHeader(HttpHeaders.CONTENT_LENGTH, result.length);
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, `attachment;filename=${opt.options?.filename || DEFAULT_FILENAME}`);
    response.end(result);
  }

  @Post('image-base64')
  @Header(HttpHeaders.CONTENT_DISPOSITION, MimeType.TEXT.PLAIN)
  async getImageInBase64(@Body() opt: Options): Promise<string> {
    return BufferUtils.toBase64(await this.echartsService.getImage(opt));
  }
}

The code does not compile because of a dependency is not installed yet. This dependency is @yggdrasilts/volundr, part of our toolset. This library is a set of utilities for TypeScript developments. To compile, you only need to install it using npm i --save @yggdrasilts/volundr.

If you want to know more about it, take a look to its repository @yggdratilsts/volundr

If you see the code, it is very easy to understand it because the NestJS decorators are very explicit.

  • @post () decorator indicates that both endpoints are listening POST request.
  • I am using the @Header() decorator to set the response Headers.
  • And also I am using @Body() decorator to get the request body to be used inside the service.

At this point, if you start the service npm run start:dev you will have available the wanted endpoints:

I'm getting the *echartOptions data from the ECharts Example - Stacked Area Chart*

'End of Part 2' You can check the project code in the part-2 tag

Part 3 - Logger, Validations and Pipes, Handling Errors and Modules

Now, the application is functional but before to finish, I would like to add, and talk, about some other features that NestJS provides to create a service.

Logger

In my opinion, every project should have a good logger implementation because it is the principal way to know what is happening in the flow. In this case, NestJS has a built-in text-based Logger that is enough for our service.

To use this logger, it is only necessary to instantiate the class and start using it. For example:

import { Controller, Get, Logger } from '@nestjs/common';

@Controller()
export class MyController {
  private readonly logger = new Logger(MyController.name);

  @Get('log-data')
  getImage(): void {
      this.logger.debug('Logging data with NestJS it's so easy...');  
  }
}

The service that I am creating will use this logger for the app.controller.tsbody.validation.pipe.ts and http-exception.filter.ts. I am going to talk about these two last files in the following parts of the post.

If you want to know more about how to use the NestJS logger, you can go to its documentation. Also, in the following posts, I will talk about it.

Validations and Pipes

Like NestJS documentation says in its Validation section, it is best practice to validate the correctness of any data sent into a web application. For this reason, I am going to use the Object Schema Validation and creating a custom Pipe to do so.

First, I am going to adapt the project installing some needed dependencies and creating and modifying some files and folders.

Installing needed dependencies

  • npm i --save @hapi/joi
  • npm i --save-dev @types/hapi__joi

@hapi/joi lets you describe your data using a simple, intuitive, and readable language

Creating new files and folders

  • src/pipes/body.validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, Logger } from '@nestjs/common';

import { Schema } from '@hapi/joi';

/**
 * Pipe to validate request body.
 */
@Injectable()
export class BodyValidationPipe implements PipeTransform {
  private readonly logger = new Logger(BodyValidationPipe.name);

  constructor(private readonly schema: Schema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    this.logger.debug(`Input body: ${JSON.stringify(value)}`);
    const { error } = this.schema.validate(value);
    if (error) {
      this.logger.error(`Error validating body: ${JSON.stringify(error)}`);
      throw new BadRequestException(`Validation failed: ${error.message}`);
    }
    return value;
  }
}

Modifying app.controller.ts

import { Controller, Post, Header, Body, Res, UsePipes, Logger } from '@nestjs/common';
import { Response } from 'express';

import { HttpHeaders, MimeType, BufferUtils } from '@yggdrasilts/volundr';

import { EchartsService } from './echarts/echarts.service';
import { Options } from './echarts/entities/options.entity';
import { DEFAULT_FILENAME, IMAGE_BODY_VALIDATION_SCHEMA } from './echarts/constants';

import { BodyValidationPipe } from './pipes/body.validation.pipe';

@Controller()
export class AppController {
  private readonly logger = new Logger(AppController.name);

  constructor(private readonly echartsService: EchartsService) {}

  @Post('image')
  @Header(HttpHeaders.CONTENT_TYPE, MimeType.IMAGE.PNG)
  @UsePipes(new BodyValidationPipe(IMAGE_BODY_VALIDATION_SCHEMA))
  async getImage(@Body() opt: Options, @Res() response: Response): Promise<void> {
    const result = await this.echartsService.getImage(opt);
    response.setHeader(HttpHeaders.CONTENT_LENGTH, result.length);
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, `attachment;filename=${opt.options?.filename || DEFAULT_FILENAME}`);
    response.end(result);
  }

  @Post('image-base64')
  @Header(HttpHeaders.CONTENT_DISPOSITION, MimeType.TEXT.PLAIN)
  @UsePipes(new BodyValidationPipe(IMAGE_BODY_VALIDATION_SCHEMA))
  async getImageInBase64(@Body() opt: Options): Promise<string> {
    return BufferUtils.toBase64(await this.echartsService.getImage(opt));
  }
}

Modifying echarts/constants.ts

/**
 * Application constants.
 */
import * as Joi from '@hapi/joi';

export const DEFAULT_IMAGE_WIDTH = 600;

export const DEFAULT_IMAGE_HEIGHT = 250;

export const DEFAULT_FILENAME = 'echarts.png';

export const IMAGE_BODY_VALIDATION_SCHEMA = Joi.object({
  echartOptions: Joi.object().required(),
  options: Joi.object().optional(),
});

Now, if you start the service npm run start:dev and do a request with an invalid body, you will see an error.

This is a simple error handling that NestJS provides by default but I like to customize it and show more readable error using NestJS Exceptions Filters.

'End of Part 3.1' You can check the project code in the part-3.1 tag

Handling Errors

First, I am going to create my custom Exception Filter and activated it in the global-scope to manage all endpoint errors.

To do this purpose, I am going to create a new folder and file called src/exceptions/http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

import { HttpHeaders, MimeType } from '@yggdrasilts/volundr';

/**
 * Filter to catch HttpException manipulating the response to get understandable response.
 */
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const message = exception.message;

    const errorData = {
      timestamp: new Date().toISOString(),
      message,
      details: {
        request: {
          method: request.method,
          query: request.query,
          body: request.body,
        },
        path: request.url,
      },
    };
    this.logger.error(`${JSON.stringify(errorData)}`);
    response.setHeader(HttpHeaders.CONTENT_TYPE, MimeType.APPLICATION.JSON);
    response.status(status).json(errorData);
  }
}

This filter is similar to the one in NestJS Exception Filter section, but I have made some personal customizations to get more information in the error data.

After the file creation, it is needed to activate this filter in the global-scope. To do so, I am going to modify the main.js file:

import { NestFactory } from '@nestjs/core';

import { AppModule } from './app.module';

import { HttpExceptionFilter } from './exceptions/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(3000);
}
bootstrap();

Now, you can start the service again npm run start:dev and see how the error has changed.

'End of Part 3.2' You can check the project code in the part-3.2 tag

Modules

To finalize this part, I am going to use modules to follow the structure that NestJS propose because I think this structure it is very intuitive to follow having different modules with their own functionality.

To do this, I am going to create the src/echarts/echarts.module.ts file:

import { Module } from '@nestjs/common';

import { EchartsService } from './echarts.service';

@Module({
  providers: [EchartsService],
  exports: [EchartsService],
})
export class EchartsModule {}

And modify the app.module.ts file to import this new module instead of importing the EcharsService directly:

import { Module } from '@nestjs/common';

import { EchartsModule } from './echarts/echarts.module';

import { AppController } from './app.controller';

@Module({
  imports: [EchartsModule],
  controllers: [AppController],
})
export class AppModule {}

Finally, I am going to check if the service continues working as before executing npm run start:dev and checking the endpoints with the REST Client VS extension like at the end of Part 2.

'End of Part 3.3' You can check the project code in the part-3.3 tag

Part 4 - Swagger

Providing good documentation for every API is essential if we want to be used by other people. There are lots of alternatives to create this documentation but NestJS has an awesome module that use Swagger for this purpose.

Kamil MysliwiecNestJS creator, has written a great article talking about the new NestJS Swagger module features. I recommend you to read it, it is very interesting.

Before to start documenting, it is necessary to install the NestJS Swagger module:

odin@asgard:~/Blog $ npm install --save @nestjs/swagger swagger-ui-express

Once installed, we bootstrap the service modifying the main.ts file like the documentation sais:

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

import { AppModule } from './app.module';

import { HttpExceptionFilter } from './exceptions/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalFilters(new HttpExceptionFilter());

  const options = new DocumentBuilder()
    .setTitle('Echarts API')
    .setDescription('API to get charts, using echartsjs, as image file.')
    .setExternalDoc('More about echartsjs', 'https://echarts.apache.org/en/index.html')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

Now we can start the service npm run start:dev and automatically we will have the Swagger documentation at http://localhost:3000/api:

Don't you think this is so easy? 😉

If you are read the above Kamil Mysliwiec's post, with the new NestJS Swagger plugin version, the NestJS CLI has the ability to search in all of our code and create the Swagger documentation automatically. To do so, the only thing that we need to modify is the nest-cli.json file adding the following lines:

{
  ...
  "compilerOptions": {
    "plugins": ["@nestjs/swagger/plugin"]
  }
}

Once added, we can run the service again npm run start:dev but in this case, we are going to see the following error:

This is because the service is using the EchartOption and the NestJS Swagger plugin is not able to parse. Also easy to solve. I am going to document this particular object by myself adding the following NestJS Swagger decorators to the echartOptions variable inside the src/echarts/entities/options.entity.ts file:

import { ApiProperty } from '@nestjs/swagger';

import { EChartOption } from 'echarts';

import { ImageOptions } from './image-options.entity';

/**
 * Class to configure echarts options.
 */
export class Options {
  @ApiProperty({
    type: 'EChartOption',
    description: 'Chart configuration.',
    externalDocs: { url: 'https://echarts.apache.org/en/option.html' },
  })
  echartOptions: EChartOption;

  options?: ImageOptions;
}

Now, we can run the service again npm run start:dev and the Swagger documentation will have changed and we will have the new properties:

As I said before, don't you think this is so easy?

End of Part 4. You can check the project code in the part-4 tag

To Sum Up

As I said at the begging of this post, I am in love with NestJS because it is an awesome framework that helps you to create backend services in an efficient, reliable and scalable way, using TypeScript, with which I am in love as well. In this post, it has been shown several parts of NestJS framework and how it helps you to build a backend service easily.

I hope you have enjoyed reading it and learn, improve or discover this great framework. In the following link, I leave you the Github repository where you can find the project code.

GitHub logo yggdrasilts / echarts-api-service

This project is the sample used in the Create an API service using nestframework post showing how to create an API service using NestJS framework that use a service to return an ECharts image chart.

Echarts API Service

Description

This project is the sample used in the Create an API service using nestframework post showing how to create an API service using NestJS framework that use a service to return an ECharts image chart.

Requirements

OS Command
OS X brew install pkg-config cairo pango libpng jpeg giflib
Ubuntu sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
Fedora sudo yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel
Solaris pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto

Installation

$ npm install

Running the app

# development
$ npm run start

# watch mode
$ npm run start:dev

Test

# e2e tests
$ npm run test:e2e

Stay in touch

License

This sample is MIT licensed.

Enjoy!!

Posted on by:

telco2011 profile

David (ダビッド )

@telco2011

Telecom engineer, developer enthusiastic 👾, inline skates lover, biker 🏍 and barista initiated ☕️

YggdrasilTS

TypeScript development toolset. A set of libraries, written in TypeScript, to help its developers to build their code. 🌳

Discussion

markdown guide