Modeling the architecture for an Express Rest API can be a daunting task. The larger your application gets, the hard it is to maintain your codebase because of all of the possible configurations. Luckily, there is a framework called NestJS that takes many of the conventions of Angular, places them on top of Express JS and uses them to create a more consistently designed application architecture. Many of the benefits include consistent ways of handling documentation using swagger, type checking and dependency injection with Typescript and compatibility with the majority of all Express middleware. In this tutorial, we’ll go over how to build a simple CRUD application with Swagger Documentation using NestJS and Swagger.

First let’s make sure that the NestJS cli is installed and create a new application. This makes it easier to create the different components needed for NestJS.

npm i -g @nestjs/cli

nest new todo-api

If you’re familiar with Angular, you’ll see that there is a similar project with models, services, and controllers.

NestJs Project Structure

NestJs Project Structure

Add Dependencies and Database Configuration

If you know what dependencies you’re going to use, it’s best to add them in the beginning

npm install -s @nestjs/mongoose mongoose @nestjs/swagger

npm install --save-dev @types/mongoose

These will install the dependencies necessary for mongoose and swagger functionality.

Using Mongo DB with Mongoose

Next, lets set up the database. Inside of you app.module.ts, set up the initial connection

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest'),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

We will also want to remove the AppController and AppService files because we will not be using them

Creating our Todos Domain

We’ll want to create an area specifically for our todos domain. This includes out Database Schema, our Controller and DTO(Data Transfer Object)

In the command line run

nest generate module todos 
nest generate controller todos
nest generate service todos

When you generate the service, it will be in the main folder, move that to the todos folder.

Mongoose Database Schema

Now we must create our mongoose database schema and register it with our module. Inside of the todos folder, create another folder called schemas and create a todo.schema.ts file.

import * as mongoose from 'mongoose';

export const TodoSchema = new mongoose.Schema({
    text: String,
    complete: Boolean,
});

Create an interfaces folder with an itodos.service.ts file and todos.inteface.ts and index.ts The idotos.service.ts file specifies the calls to your datastore that you’ll be using in your service. Using this interface is good on the off-chance you switch from a NoSQL to SQL database or change your database provider. The todos.interface.ts file specifies the type of document your todos are. In our case, there are MongoDB documents created using Mongoose. Then in the index.ts file can export them both and use them later on with destructuring.

import { Document } from 'mongoose';

export interface ITodo extends Document {
    readonly text: string;
    readonly complete: boolean;
}
import { ITodo } from './todos.interface';

export interface ITodosService {
    findAll(): Promise<ITodo[]>;
    findById(ID: number): Promise<ITodo | null>;
    findOne(options: object): Promise<ITodo | null>;
    create(todos: ITodo): Promise<ITodo>;
    update(ID: number, newValue: ITodo): Promise<ITodo | null>;
    delete(ID: number): Promise<string>;
}
export * from './itodos.service';
export * from './todos.interface';

We will also create a createTodo.dto.ts in the dto folder. We’ll import ApiModelProperty from @nestjs.swagger so when we generate our Swagger documentation, Swagger knows what the different properties of our model are.

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

export class CreateTodoDto {
    @ApiModelProperty()
    readonly _id: number;

    @ApiModelProperty()
    readonly text: string;

    @ApiModelProperty()
    readonly complete: boolean;
}

Building Out Our Database service

We’ll need a way to gain access to Mongoose ORM methods for our Todo Document in a manner that conforms with DRY(Don’t-Repeat-Yourself). The best way to do this is to create a service that allows for us to call reusable database queries that can be passed around our application. NestJS allows for Dependency Injection using the Injectable Decorator. This way, we can take a class and place an instance of it to be used inside of another class. Inside of out TodosService, import mongoose as well as your models and service interfaces. Also, make sure to the to modules that allow for dependency injection in your Service class.

import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { ITodo, ITodosService } from './interfaces/index';
import { CreateTodoDto } from './dto/createTodo.dto';
import { debug } from 'console';

Next, make your service class injectable with the @Injectable decorator, add the interface to service to make sure all of the queries conform to the basic crud functions and inject our iTodo interface.

@Injectable()
export class TodosService implements ITodosService{
    constructor(@InjectModel('Todo') private readonly todoModel: Model<ITodo>) { }
}

Next, create all of your basic crud functions using the MongoDBB database queries.

async findAll(): Promise<ITodo[]> {
        return await this.todoModel.find().exec();
    }

    async findOne(options: object): Promise<ITodo> {
        return await this.todoModel.findOne(options).exec();
    }

    async findById(ID: number): Promise<ITodo> {
        return await this.todoModel.findById(ID).exec();
    }
    async create(createTodoDto: CreateTodoDto): Promise<ITodo> {
        const createdTodo = new this.todoModel(createTodoDto);
        return await createdTodo.save();
    }

    async update(ID: number, newValue: ITodo): Promise<ITodo> {
        const todo = await this.todoModel.findById(ID).exec();

        if (!todo._id) {
            debug('todo not found');
        }

        await this.todoModel.findByIdAndUpdate(ID, newValue).exec();
        return await this.todoModel.findById(ID).exec();
    }
    async delete(ID: number): Promise<string> {
        try {
            await this.todoModel.findByIdAndRemove(ID).exec();
            return 'The todo has been deleted';
        }
        catch (err){
            debug(err);
            return 'The todo could not be deleted';
        }
    }

Building the NestJS Controller with Swagger Documentation

Next, we will want to build our controller out.

We’ll need to import out Request Types, our todosService, and our DTO as well as our Decorators for our Swagger Documentation

import { Controller, Get, Response, HttpStatus, Param, Body, Post, Request, Patch, Delete } from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto} from './dto/createTodo.dto';
import { ApiUseTags, ApiResponse } from '@nestjs/swagger';

.

Next, we will tag our controller with our baseURL ‘todos’ as well as our tag for swagger

@ApiUseTags('todos')
@Controller('todos')
export class TodosController {
}

In order to access our mongoDB datastore, we’ll have to Inject our TodosService.

constructor(private readonly todosService: TodosService) {}

Now we can create our route that map to our TodosService, Each decorator annotates what type of request is being made and the URL path.

@Get()
    public async getTodos(@Response() res) {
        const todos = await this.todosService.findAll();
        return res.status(HttpStatus.OK).json(todos);
    }

    @Get('find')
    public async findTodo(@Response() res, @Body() body) {
        const queryCondition = body;
        const todos = await this.todosService.findOne(queryCondition);
        return res.status(HttpStatus.OK).json(todos);
    }

    @Get('/:id')
    public async getTodo(@Response() res, @Param() param){
        const todos = await this.todosService.findById(param.id);
        return res.status(HttpStatus.OK).json(todos);
    }

    @Post()
    @ApiResponse({ status: 201, description: 'The record has been successfully created.' })
    @ApiResponse({ status: 403, description: 'Forbidden.' })
    public async createTodo(@Response() res, @Body() createTodoDTO: CreateTodoDto) {

        const todo = await this.todosService.create(createTodoDTO);
        return res.status(HttpStatus.OK).json(todo);
    }

    @Patch('/:id')
    public async updateTodo(@Param() param, @Response() res, @Body() body) {

        const todo = await this.todosService.update(param.id, body);
        return res.status(HttpStatus.OK).json(todo);
    }

    @Delete('/:id')
    public async deleteTodo(@Param() param, @Response() res) {

        const todo = await this.todosService.delete(param.id);
        return res.status(HttpStatus.OK).json(todo);
    }

Registering Components in our NestJS Module

Now that we’ve created all of our components, we’ll have to register everything with NestJS for them to work. Even though we can register everything inside of our AppModule, it’s best to register them inside of there respective domain-specific module class.

Inside of your TodosModule, import and add you Schema, Controller, and Service.

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
import { TodoSchema } from './schemas/todo.schema';
@Module({
    imports: [MongooseModule.forFeature([{ name: 'Todo', schema: TodoSchema }])],
    controllers: [TodosController],
    providers: [TodosService],
})
export class TodosModule {}

You’ll notice that we’re importing out MongoDB schema along with giving it a name ‘Todo’

This name was also used inside of your TodosService

(@InjectModel('Todo')

This just tells NestJS to make that model to the MongoDB Schema

Now we can go back to our App Module and start connecting everything.

import { Module, NestModule } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { TodosModule } from './todos/todos.module';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest'),
    TodosModule,
],
  controllers: [],
  providers: [],
})
export class AppModule {}

This registers all of our controllers and services inside of our TodosModule with our main AppModule.

Now you go to your main.ts file and register Swagger.

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 options = new DocumentBuilder()
    .setTitle('Todos example')
    .setDescription('The Todos API description')
    .setVersion('1.0')
    .addTag('todos')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);

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

This will set up swagger with the /api url. Once everything is finished, you can now run you app with

npm start
Todo Swagger Api

Todo Swagger Api

Conclusion

NestJS offer several advantages over the standard ExpressJS implementation. It offers a consistent structure, type-checking easier integration with express middleware. it follows a set of conventions that make the application easy to follow. In the next tutorial, we’ll go over adding JWT authentication using Passport. You can view the GitHub repo here and video courses here if you’re interested in learning more.

Codebrains Newsletter

Get weekly dev news and tutorials.

Powered by ConvertKit