NestJS, Modules and Swagger best practices

Nestjs, not like any other nodejs frameworks, has many handy tools, libraries and features that let us write our programs following clean code and scalable architecture. In this article we’ll find out how to use swagger for documentation of our application, and also know the best practices for module creation.

Installation dependencies

Install nestjs cli globally for future creating projects
npm i -g @nestjs/cli

Initialize new project using following command

nest new app

Install swagger dependencies
npm install --save @nestjs/swagger swagger-ui-express

Create a simple module

Nestjs promotes module architecture like angular. Each module should be independent and designed so that it can be used in any application.

We will create a typically cats module as an example. The architectural module includes Entity, Service, Repository, Controller, DTO.

Entity

Let’s describe our entity. Entity is a representative of some real object. If there is a cat in the world, then in our program it is described by using an entity.

// src/module/cat/entity/Cat.ts
export class Cat {
    id: number;
    name: string;
    weight: number;

    constructor(id: number, name: string, weight: number) {
        this.id = id;
        this.name = name;
        this.weight = weight;
    }
}

Service

You need to describe the logic somewhere, how the code is created, how to get the list of cats, or take one cat. For this, services are used.

// src/module/cat/service/CatService.ts

import { Cat } from "../entity/Cat";
import { CreateCatData } from "../dto/CreateCatData";
import { Injectable, NotFoundException } from "@nestjs/common";

@Injectable()
export class CatService {

    private readonly cats: Cat[] = [];

    public async add(data: CreateCatData): Promise<Cat> {
        const cat = new Cat(this.cats.length, data.name, data.weight);
        this.cats.push(cat);
        return cat;
    }

    public async list(): Promise<Cat[]> {
        return this.cats;
    }

    public async get(id: number): Promise<Cat> {
        const cat = this.cats.find(cat => cat.id === id);
        if(!cat) {
            throw new NotFoundException("Cat not found");
        }

        return cat;
    }
}

Make developers make a mistake at this stage. I committed it too.

Since there’s not so much code and we don’t work with connecting to the database, an array is used to store entities, we didn’t notice this problem. We described this array directly in the service, in a real project we would inject a connection to the database here and start working with it. Which would lead to a violation of the first principle of SOLID - Single Responsibility. It must be remembered that the service is responsible for business logic, and not for working with data. To work with data, another pattern is used - Repository.

Repository

// src/module/cat/repository/CatRepository.ts

import { Injectable, NotFoundException } from "@nestjs/common";
import { Cat } from "../entity/Cat";

@Injectable()
export class CatRepository {
    private readonly cats: Cat[] = [];

    public async save(cat: Cat): Promise<void> {
        this.cats.push(cat);
    }
    
    public async findAll(): Promise<Cat[]> {
        return this.cats;
    }
    
    public async get(id: number): Promise<Cat> {
        const cat = this.cats.find(cat => cat.id === id);
        if(!cat) {
            throw new NotFoundException("Cat not found");
        }

        return cat;
    }
}

Now our service works with a repository. Let`s correct code in service.

// src/module/cat/service/CatService.ts

import { Cat } from "../entity/Cat";
import { CreateCatData } from "../dto/CreateCatData";
import { Injectable } from "@nestjs/common";
import { CatRepository } from "../repository/CatRepository";

@Injectable()
export class CatService {
    constructor(
        private readonly catRepository: CatRepository
    ) {}

    public async add(data: CreateCatData): Promise<Cat> {
        const id = this.catRepository.count;
        const cat = new Cat(id, data.name, data.weight);
        await this.catRepository.save(cat);
        return cat;
    }

    public async list(): Promise<Cat[]> {
        return this.catRepository.findAll();
    }

    public async get(id: number): Promise<Cat> {
        return this.catRepository.get(id);
    }
}

Data Transfer Object ( DTO )

Let’s move on to the next pattern. This is a Data Transfer Object. Often, service methods require a lot of arguments. In this case, the Date Transfer Object pattern comes to our aid. This is an object that encloses in its properties all the arguments that the service requires. In our case, this is an “add” method.

DTO class is really simple.


// src/module/cat/dto/CreateCatData.ts
export class CreateCatData {
    name: string;
    weight: number;
}


Controller

Last pattern in this article, but not last in NestJS. It should be noted that the controller is a pattern that is responsible for accepting the input data and sending them further to the system. The classes representing the Controller can only contain user input, validation, launching the appropriate service method, and generating a response.

In our case controller looks like


// src/module/cat/controller/CatController.ts

import { Body, Controller, Get, Param, ParseIntPipe, Post } from "@nestjs/common";
import { CatService } from "../service/CatService";
import { Cat } from "../entity/Cat";
import { CreateCatData } from "../dto/CreateCatData";

@Controller()
export class CatController {
    constructor(
        private readonly catService: CatService
    ) {}

    @Post("/cat")
    public async create(@Body() data: CreateCatData): Promise {
        return this.catService.add(data);
    }

    @Get("/cats")
    public async list(): Promise {
        return this.catService.list();
    }

    @Get("/cat/:id")
    public async get(@Param("id", ParseIntPipe) id: number): Promise {
        return this.catService.get(id);
    }
}

Module

Now we need to enclose in a module all that we’ve described.


// src/module/cat/CatModule.ts

import { Module } from "@nestjs/common";
import { CatController } from "./controller/CatController";
import { CatService } from "./service/CatService";
import { CatRepository } from "./repository/CatRepository";

@Module({
    controllers: [
        CatController
    ],
    providers: [
        CatService,
        CatRepository
    ],
    exports: [
        CatService,
        CatRepository
    ]
})
export class CatModule {}

Controllers specify in the “controllers” section. On the compiling stage nestjs will use these classes and provide http interface.

“providers” is an array of classes that are used in our module. On the compiling stage nestjs will create objects automatically with resolving constructor dependencies.

“exports” - in this section you can specify providers classes that can be used in providers other modules.

Module import.

Go to AppModule and import our module. Specify our module in “imports” section and try to run the application using following command npm run start:dev

In the logs, we can observe that the module has connected our module and provided the routes for interaction via HTTP with our controller.

Works fine !

Swagger

Let’s add documentation to our module and application as a whole.

You need to connect the swagger module for this in “main.ts” so you should write the following code


// src/main.ts

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

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

  const options = new DocumentBuilder()
      .setVersion('1.0')
      .build();

  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('/docs', app, document);

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

Go to “localhost:3000/docs” and you can already see that your controller has a documentation. But it’s still not clear how to work with your API. Add clarity.

Add decorators on properties in DTO.

// src/module/cat/dto/CreateCatData.ts

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

export class CreateCatData {
    @ApiProperty({
        required: true,
        type: 'string',
        name: 'name',
        description: 'name of your cat'
    })
    name: string;

    @ApiProperty({
        required: true,
        type: 'number',
        name: 'weight',
        description: 'weight of you cat'
    })
    weight: number;
}

Now we can see request schema on “POST /cat”

Document our controller.


// src/module/cat/controller/CatController.ts

@Controller()
@ApiTags("Cats")
export class CatController {
    constructor(
        private readonly catService: CatService
    ) {}

    @Post("/cat")
    @ApiResponse({type: Cat, status: 201})
    public async create(@Body() data: CreateCatData): Promise {
        return this.catService.add(data);
    }

    @Get("/cats")
    @ApiResponse({type: Cat, isArray: true, status: 200})
    public async list(): Promise {
        return this.catService.list();
    }

    @Get("/cat/:id")
    @ApiResponse({type: Cat, status: 200})
    @HttpCode(200)
    public async get(@Param("id", ParseIntPipe) id: number): Promise {
        return this.catService.get(id);
    }
}

Thanks to ApiTags we have added a separate list under our heading, which includes all the methods of the Controller. Therefore we got an empty response schema, and we’re going to fix it - by adding a description of properties to our entity.

// src/module/cat/entity/Cat.ts

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

export class Cat {
    @ApiProperty({
        type: 'number',
        name: 'id',
        description: 'id of cat'
    })
    id: number;

    @ApiProperty({
        type: 'string',
        name: 'name',
        description: 'name of cat'
    })
    name: string;

    @ApiProperty({
        type: 'number',
        name: 'weight',
        description: 'weight of cat'
    })
    weight: number;

    constructor(id: number, name: string, weight: number) {
        this.id = id;
        this.name = name;
        this.weight = weight;
    }
}

Now, when we open our route, we have a complete vision of how it works, what kind of request should be sent, and what will be the response.

We can send requests directly from swagger. To do this click “try in out”.

We examined how to create modules and document them. Good luck !