Building a REST API using Nest.js and Swagger


Introduction

Friends, hello everyone!

My name is Alexey and I have been doing frontend development for some time now.

In this article, I will describe one of the ways to implement an application that provides a RESTfull API. I will briefly talk about how I wrote a similar application in Typescript, as well as give code examples. The existence of such an article would greatly facilitate my life when working on a project, I hope my article will help you too!

A little background

To test hypotheses during product development, it is required to implement a prototype of some application in a short time. As part of my work tasks, I happened to work on a similar prototype. It was a backend application providing a RESTfull API and implemented using Nest.js and Swagger technologies.

Choice of technologies

When choosing a stack, the key requirement was the use of Node.js, since the task of quickly implementing the RESTfull API fell on the shoulders of the frontend development team. At the same time, the team usually uses the Angular framework as the main tool.

Therefore, Nest.js turned out to be an ideal candidate, as the creators of this framework were inspired by the approaches used in Angular. Here we have the usual Dependency Injection, RxJS, Typescript, a module system and a powerful CLI. The resulting API was decided to be documented using Swagger.

Briefly about technologies

Nest (NestJS) — a framework for developing efficient and scalable server applications on Node.js. This framework uses progressive (which means the current version ECMAScript) JavaScript with full support typescript (usage typescript is optional) and combines elements of object-oriented, functional, and reactive functional programming.

under the hood Nest uses Express (default), but also allows you to use fastify.

I recommend reading more about Nest here.

Swagger is a set of tools that help describe the API. Thanks to him, users and machines better understand the possibilities REST API without access to the code. With Swagger, you can quickly create documentation and send it to other developers or clients.

I recommend reading more about Swagger here.

Preparing and setting up a project

As I noted above, Nest.js comes with a fairly powerful CLI. Let’s start by installing it, making sure that we have installed on the machine Node.js and npm. To install, run the command:

$ npm i -g @nestjs/cli

After the CLI is installed, we will use it to create a template for our application with the name rest-api-with-swagger:

$ nest new rest-api-with-swagger

In Nest.js, the entities that are responsible for handling incoming HTTP requests and generating responses are called controllers. Below is an example code from a controller (app.controller.ts) created by default when generating a template project:

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

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

In turn, the entities that implement the business logic of the application are called services:

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Services and controllers (as well as pipes (Pipes)guards (Guards) and other entities) are combined into modules (Modules) – the building blocks from which the final application is formed.

REST API

Let our resource, to which we want to provide access through the developed API, be notes (notes). In order not to waste time creating the necessary entities, we will use the following command:

$ nest g resource notes # или nest generate resource notes

This command will create an application module dedicated to working with notes and automatically connect it to our application. The file structure of this module will look like this:

src
	notes
		|-- dto 
  			-- create-note.dto.ts
				-- update-note.dto.ts
		|-- entities
  			-- note.entity.ts
		-- notes.controller.spec.ts 
  	-- notes.controller.ts
  	-- notes.service.spec.ts
  	-- notes.service.ts
  	-- notes.module.ts
...

The controller code that will process requests to perform operations on notes looks something like this:

import { 
  Controller, Get, Post, 
  Body, Patch, Param, Delete, Query 
} from '@nestjs/common';
import { NotesService } from './notes.service';
import { CreateNoteDto } from './dto/create-note.dto';
import { UpdateNoteDto } from './dto/update-note.dto';

// Все запросы, содержащие в пути /notes, будут перенаправлены в этот контроллер
@Controller('notes')
export class NotesController {
  constructor(private readonly notesService: NotesService) {}

  @Post() // обработает POST http://localhost/notes?userId={userId}
  create(
  	@Query('userId') userId: number, // <--- достанет userId из query строки  
   	@Body() createNoteDto: CreateNoteDto
	) {
    return this.notesService.create(userId, createNoteDto);
  }

  @Get() // обработает GET http://localhost/notes?userId={userId}
  findAll(@Query('userId') userId: number) {
    return this.notesService.findAll(userId);
  }

  @Get(':noteId') // обработает GET http://localhost/notes/{noteId}
  findOne(@Param('noteId') noteId: number) {
    return this.notesService.findOne(noteId);
  }

  @Patch(':noteId') // обработает PATCH http://localhost/notes/{noteId}
  update(@Param('noteId') noteId: number, @Body() updateNoteDto: UpdateNoteDto) {
    return this.notesService.update(noteId, updateNoteDto);
  }

  @Delete(':noteId') // обработает DELETE http://localhost/notes/{noteId}
  remove(@Param('noteId') noteId: number) {
    return this.notesService.remove(noteId);
  }
}

Just like that, we just got a ready-made controller, the operation of which complies with all the necessary rules for building a REST API. Next, we implement the logic of working with our notes in NotesService. To simplify, the notes will be stored in an array. In the case of a real application, in this service it would be necessary to implement the logic of accessing the note storage service (for example, a database), but this is a topic for another article. You can read more here.

First of all, let’s fill the models (CreateNoteDto, UpdateNoteDto and note) that describe the notes themselves and what to do with them. As a result, the service code will look like this:

import { Injectable } from '@nestjs/common';
import { CreateNoteDto } from './dto/create-note.dto';
import { UpdateNoteDto } from './dto/update-note.dto';
import { Note } from './entities/note.entity';

@Injectable()
export class NotesService {

  private _notes: Note[] = [];

  create(userId: number, dto: CreateNoteDto) {
    const id = this._getRandomInt();
    const note = new Note(id, userId, dto.title, dto.content);
    this._notes.push(note);
    return note;
  }

  findAll(userId: number) {
    return this._notes.filter(note => note.userId == userId);
  }

  findOne(noteId: number) {
    return this._notes.filter(note => note.id == noteId);
  }

  update(noteId: number, dto: UpdateNoteDto) {
    const index = this._notes.findIndex(note => note.id == noteId)
    
    if (index === -1) {
      return;
    }

    const { id, userId } = this._notes[index];
    this._notes[index] = new Note(id, userId, dto.title, dto.content);
    return this._notes[index];
  }

  remove(noteId: number) {
    this._notes = this._notes.filter(note => note.id != noteId)
  }

  private _getRandomInt() {
    return Math.floor(Math.random() * 100);
  }
}

Basic application that implements CRUD operations on notes (resource), received. Now, let’s move on to documenting the API. To do this, install the Swagger module for Nest.js:

$ npm install --save @nestjs/swagger swagger-ui-express

and connect it to our application in the file main.ts:

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

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

  const config = new DocumentBuilder()
    .setTitle('Notes API')
    .setDescription('The notes API description')
    .setVersion('1.0')
    .build();
    
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}

bootstrap();

In order to make sure that our application is working, run it with the command:

$ npm run start:dev

After launch, at http://localhost:3000/api/ the following page will be displayed:

Page with Swagger representation of the received API
Page with Swagger representation of the received API

Already something, but it doesn’t look like full-fledged documentation yet.

First, let’s move all methods of working with notes into a separate section Notes. To do this, we will hang another decorator on NotesController:

@ApiTags('Notes')  // <---- Отдельная секция в Swagger для всех методов контроллера
@Controller('notes')
export class NotesController {
  ...
}

Also, we will remove from our documentation the method of working with the root route (/) by attaching a decorator to it ApiExcludeEndpoint:

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

  @Get()
  @ApiExcludeEndpoint() // <----- Скрыть метод контроллера в Swagger описании
  getHello(): string {
    ...
  }
}

In this case, the result will look like this:

Secondly, let’s add a description to our endoints, as well as validation of the received parameters. As a result, our controller methods will look like this:

@ApiTags('Notes')
@Controller('notes') 
export class NotesController {

  ...

  @Patch(':noteId') // обработает PATCH http://localhost/notes/{noteId}
  @ApiOperation({ summary: "Updates a note with specified id" })
  @ApiParam({ name: "noteId", required: true, description: "Note identifier" })
  @ApiResponse({ status: HttpStatus.OK, description: "Success", type: Note })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "Bad Request" })
  update(
    @Param('noteId', new ParseIntPipe()) noteId: number, 
    @Body() updateNoteDto: UpdateNoteDto
	) {
    return this.notesService.update(noteId, updateNoteDto);
  }

	...
}

The complete application code can be found in the repository for link. A list of decorators that allow you to describe API methods can be found at here. In the example above, to validate the input parameters of the method update pipe is used ParseIntPipemore details about it and other built-in pipes can be found at link.

To correctly form the Swagger description, you need to modify our dtoas well as the class Note:

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

export class Note {
    @ApiProperty({ description: "Note identifier", nullable: false })
    id: number;

    @ApiProperty({ description: "User identifier", nullable: true })
    userId: number;
    
    @ApiProperty({ description: "Note title", nullable: true })
    title: string;
    
    @ApiProperty({ description: "Note content", nullable: true })
    content: string;
  
	...
}

As a result, our Swagger description will look like this:

API endpoints are ready, now let’s protect our resource from unauthorized access. In my case, according to the TOR, it was required to use the access method using the API key (for more details, you can read here). Examples of authorization using JWT you can see here, here or here.

We will implement authorization using the popular library passportfor this we will use the official module (wrapper) for Nest.js – @nestjs/passport. First of all, install the required module:

$ npm install --save @nestjs/passport passport passport-headerapikey

Next, let’s create a separate module in our application responsible for authorization:

$ nest g mo authorization # nest generate module authorization

Working with the passport library is based on the use of the so-called authorization strategies. Let’s implement one of themapi-key.strategy.ts):

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import Strategy from "passport-headerapikey";

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(Strategy, "api-key") {
  
  constructor(private readonly _configService: ConfigService) {
    super({ header: "X-API-KEY", prefix: "" }, 
          true, 
          async (apiKey, done) => this.validate(apiKey, done)
         );
  }

  public validate = (incomingApiKey: string, done: (error: Error, data) => Record<string, unknown>) => {
    const configApiKey = this._configService.get("apiKey");

    if (configApiKey === incomingApiKey) {
      done(null, true);
    }

    done(new UnauthorizedException(), null);
  };
}

In the example above in the constructor ApiKeyStrategy service is injected ConfigService. This service is part of the package @nestjs/config and allows you to simplify the work with files that contain environment variables (i.e. files of the form .env). In our application, the API access key is a configuration parameter and is written in the file .env (cm. project code). For more information about working with the configuration module, see here.

Now let’s put together our authorization module (authorization.module.ts):

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { PassportModule } from "@nestjs/passport";
import { ApiKeyStrategy } from "./api-key.strategy";

@Module({
  imports: [PassportModule, ConfigModule],
  providers: [ApiKeyStrategy],
})
export class AuthorizationModule {}

The authorization module is ready, but at the moment the methods of our NotesController will continue to process requests that do not contain an API key in HTTP request headers. To protect the API, let’s add a few more decorators to the controller:

@ApiTags('Notes')
@ApiSecurity("X-API-KEY", ["X-API-KEY"]) // <----- Авторизация через Swagger 
@Controller('notes')
export class NotesController {
  constructor(private readonly notesService: NotesService) {}

  @Post() // обработает POST http://localhost/notes?userId={userId}
  @UseGuards(AuthGuard("api-key")) // <---- Вернет 401 (unauthorized)
  																 // при попытке доступа без корректного API ключа
	...
  create(
  	@Query('userId', new ParseIntPipe()) userId: number,
   	@Body() createNoteDto: CreateNoteDto
	) {
    return this.notesService.create(userId, createNoteDto);
  }
}

and modify the file main.ts:

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

  const config = new DocumentBuilder()
    .setTitle('Notes API')
    .setDescription('The notes API description')
    .setVersion('1.0')
    .addApiKey({       // <--- Покажет опцию X-API-KEY (apiKey)
      type: "apiKey",  // в окне 'Available authorizations' в Swagger
      name: "X-API-KEY", 
      in: "header", 
      description: "Enter your API key" 
		}, "X-API-KEY")
    .build();
  
  ...
}

The final Swagger description of our API will look like this:

In conclusion, I also recommend taking a look at the official code sample repository from the developers of the framework.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *