I believe that one of the reasons Nest.js was created was to provide a mature and structural design pattern to the Node.js development world. Nest.js is a scalable and efficient server-side Node.js framework built with TypeScript. It was heavily inspired by Angular.js and uses Express.js under the hood, which explains its compatibility with the majority of the Express.js middleware.

In this post, we will build a RESTful API with Nest.js. In the process, you will get conversant with its fundamental principles and building blocks. You will also learn the recommended approach to writing tests for each API endpoint. Finally, you will be able to automate the testing process using CircleCI.

Prerequisites

The following are required for you to get the most out of this tutorial:

  • Node.js installed on your computer
  • MySQL installed on your computer
  • Nest CLI installed on your computer
  • A GitHub account
  • A CircleCI account
  • Although not mandatory, you should know a few things about TypeScript.

Getting started

The RESTful API that we will build in this post will provision endpoints to create a product with a name, description, and price. We will be able to edit, delete, and retrieve a single product, as well as be able to retrieve the entire list of products saved in the database. This tutorial will also make use of MySQL as the preferred relational database choice and combine it with TypeORM. However, Nest.js is database agnostic, so you can choose to work with any database. You can find more details about databases here.

Scaffolding the Nest.js application

Run the following command to create a new application:

nest new nest-starter-testing

After running the nest command, you will be prompted to choose a package manager to use. Select npm and hit ENTER on your keyboard to start installing Nest.js. This will create a new project in a nest-starter-testing folder and install all of its required dependencies. Before running the application, use npm to install a validation library that you will use later in the tutorial:

npm install class-validator --save

Move into the application folder and start the application using the following commands:

// move into the project
cd nest-starter-testing

// start the server
npm run start:dev

This will start the application on the default 3000 port. Navigate to http://localhost:3000 in your favorite browser to view it.

Nest.js Default page

Configuring and connecting to the database

TypeORM is a popular object-relational mapper (ORM) used for TypeScript and JavaScript applications. To facilitate its integration with Nest.js applications, you will install an accompanying package for it, and also install a Node.js driver for MySQL. To do that, stop the app from running with CTRL + C and run the following command:

npm install --save @nestjs/typeorm typeorm mysql

Once the installation process is complete, you can proceed to import the TypeOrmModule into the root of the application.

Updating the root module

One of the building blocks of Nest.js is modules. Modules are TypeScript files decorated with @Module, providing the metadata that Nest.js uses to organize the application structure. The root module located in ./src/app.module.ts is the top-level module. It’s usually enough for a small application. However, it is advisable and recommended by Nest.js to break a large application into multiple modules. It helps to maintain the structure of the application.

To create a connection with the database, open the ./src/app.module.ts file and replace its content with the following code:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: DB_USER,
      password: DB_PASSWORD,
      database: 'test_db',
      entities: [join(__dirname, '**', '*.entity.{ts,js}')],
      synchronize: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Note: Replace DB_USER and DB_PASSWORD with your credentials

Here, we established a connection with the database by importing TypeOrmModule into the root AppModule and specified the connection options. These include the database details and the directory where the entity files will be stored. We will learn more about entity files in the next section.

Database connection

In the pre-requisites at the start of this tutorial, I referred to the MySQL download page. After you download, you will need to configure the DB so that it works for this application.

Login to MySQL in your terminal by running:

mysql -u root -p

Enter the password you set during MySQL installation. Now run:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

Replace ‘password’ with your password. This command sets the preferrable authentication for Node.js drivers for MySQL. To create the DB, run:

CREATE DATABASE test_db;

Creating the product module, service, and controller for the app

Now that we have successfully configured the database connection, we will start creating more structure for the application.

Generating a module

Start by generating a module for Product. This will be a new module used to group all items related to the product. Begin by running the following command:

nest generate module product

The command above will create a new product folder within the src directory, define the ProductModule in the product.module.ts file, and automatically update the root module in the app.module.ts file by importing the newly created ProductModule. The ./src/product/product.module.ts file will be empty for now as shown below.

import { Module } from '@nestjs/common';
@Module({})
export class ProductModule {}

Creating an entity

To create a proper database schema for a Nest.js application, TypeORM supports the creation of an entity. An entity is a class that maps to a particular database table. In this case, the product table. We will follow the proper structure for a Nest.js app by creating an entity file in the src/product directory since it is related to product.

To begin, create a new file within the src/product folder and name it product.entity.ts. Then paste the following code into it:

import { PrimaryGeneratedColumn, BaseEntity, Column, Entity } from 'typeorm';

@Entity()
export class Product extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  price: string;
}

Using the decorators imported from the typeorm module, we created four columns for the product table. Among them is the primary key column to uniquely identify a product.

Creating a data transfer object

A data transfer object (DTO) helps to create and validate a proper data structure for data coming into an application. For example, whenever you send an HTTP POST request from the front end to a Node.js back end, you need to extract the content posted from the form and parse it into a format that your back end code can easily consume. DTO helps to clearly specify shapes of objects extracted from the body of a request when communicating with the back end, and provides a way to plug in validation with little effort.

To set up the DTO for this application, create a new folder within the src/product directory and name it dto. Next, create a file within the newly created folder and call it create-product.dto.ts. Use the following content for it:

import { IsString } from 'class-validator';

export class CreateProductDTO {
  @IsString()
  name: string;

  @IsString()
  description: string;

  @IsString()
  price: string;
}

Here, we have defined a class to represent CreateProductDTO and also added a bit of validation to ensure that the data type of the fields is string. Next, we will create a repository to help persist data directly into our application database.

Creating a custom repository

Generally, a repository in ORMs such as TypeORM functions mainly as a persistence layer. It contains methods such as:

  • save()
  • delete()
  • find()

This helps to communicate with the database of an application. In this tutorial, we will create a custom repository that extends the base repository of TypeORM for our product entity and create some custom methods for specific queries. Start by navigating to the src/product folder and create a new file named product.repository.ts. Once you’re done, paste the following content into it:

import { Repository, EntityRepository } from 'typeorm';
import { Product } from './product.entity';
import { CreateProductDTO } from './dto/create-product.dto';

@EntityRepository(Product)
export class ProductRepository extends Repository<Product> {

  public async createProduct(
    createProductDto: CreateProductDTO,
  ): Promise<Product> {
    const { name, description, price } = createProductDto;

    const product = new Product();
    product.name = name;
    product.description = description;
    product.price = price;

    await product.save();
    return product;
  }

  public async editProduct(
    createProductDto: CreateProductDTO,
    editedProduct: Product,
  ): Promise<Product> {
    const { name, description, price } = createProductDto;

    editedProduct.name = name;
    editedProduct.description = description;
    editedProduct.price = price;
    await editedProduct.save();

    return editedProduct;
  }
}

From the code above, we defined two methods:

  • createProduct(): This method takes the createProductDto class, which will be used to extract the body of the HTTP request, as an argument. Then we destructure createProductDto and use the values to create a new product.
  • editProduct: Here, the details of the product that needs to be edited are passed to this method and based on the new values from the client-side, the specified details will be updated and saved in the database accordingly.

Generating a service

A service, also known as a provider, is another building block in Nest.js that is categorized under the separation of concerns principle. It is designed to handle and abstract complex business logic away from the controller and return the appropriate responses. All services in Nest.js are decorated with the @Injectable() decorator and this makes it easy to inject services into any other file, such as controllers and modules.

Create a service for product using the following command:

nest generate service product

After running the command above, you will see the following output on the terminal.

CREATE /src/product/product.service.spec.ts (467 bytes)
CREATE /src/product/product.service.ts (91 bytes)
UPDATE /src/product/product.module.ts (167 bytes)

What happened here is that the nest command has created two new files within the src/product folder. These are:

  • product.service.spec.ts: This file will be used to write unit tests for the methods that will be created within the product service file.
  • product.service.ts: This is the product service file that will hold all the business logic for the application.

It has also automatically imported the newly created service and added it to the product.module.ts file.

Next, you will populate the product.service.ts file with methods for creating a product, retrieving all created products, as well as fetching, updating, and deleting the details of a particular product. Open the file and replace its content with the following:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Product } from './product.entity';
import { CreateProductDTO } from './dto/create-product.dto';
import { ProductRepository } from './product.repository';

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(ProductRepository)
    private productRepository: ProductRepository,
  ) {}

  public async createProduct(
    createProductDto: CreateProductDTO,
  ): Promise<Product> {
    return await this.productRepository.createProduct(createProductDto);
  }


  public async getProducts(): Promise<Product[]> {
    return await this.productRepository.find();
  }


  public async getProduct(productId: number): Promise<Product> {
    const foundProduct = await this.productRepository.findOne(productId);
    if (!foundProduct) {
      throw new NotFoundException('Product not found');
    }
    return foundProduct;
  }


  public async editProduct(
    productId: number,
    createProductDto: CreateProductDTO,
  ): Promise<Product> {
    const editedProduct = await this.productRepository.findOne(productId);
    if (!editedProduct) {
      throw new NotFoundException('Product not found');
    }
    return this.productRepository.editProduct(createProductDto, editedProduct);
  }


  public async deleteProduct(productId: number): Promise<void> {
    await this.productRepository.delete(productId);
  }
}

Here, we imported the required modules for the application and created individual methods to:

  • createProduct(): Create a new product
  • getProducts(): Get all created products
  • getProduct(): Retrieve the details of a single product
  • editProduct(): Edit the details of a particular product
  • deleteProduct(): Delete a single product

It’s also important to note that we injected the ProductRepository we created earlier into this service in order to easily interact and communicate with the database. Below is a snippet of the file showing this bit.

  ...

  constructor(
    @InjectRepository(ProductRepository)
    private productRepository: ProductRepository,
  ) {}

  ...

This will only work if we also import the ProductRepository into the product module. We will do this later in the tutorial.

Generating a controller

The responsibility of controllers in Nest.js is to receive and handle the incoming HTTP requests from the client side of an application and return the appropriate responses based on the business logic. The routing mechanism, which is controlled by the decorator @Controller() attached to the top of each controller, usually determines which controller receives which requests. To create a new controller file for our project, run the following command from the terminal:

nest generate controller product --no-spec

You will see the following output.

CREATE /src/product/product.controller.ts (103 bytes)
UPDATE /src/product/product.module.ts (261 bytes)

Because we won’t be writing a test for this controller, we used the --no-spec option to instruct the nest command not to generate a .spec.ts file for the controller. Open the src/product/product.controller.ts file and replace its code with the following:

import {
  Controller,
  Post,
  Body,
  Get,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { ProductService } from './product.service';
import { CreateProductDTO } from './dto/create-product.dto';
import { Product } from './product.entity';

@Controller('product')
export class ProductController {
  constructor(private productService: ProductService) {}

  @Post('create')
  public async createProduct(
    @Body() createProductDto: CreateProductDTO,
  ): Promise<Product> {
    const product = await this.productService.createProduct(createProductDto);
    return product;
  }

  @Get('all')
  public async getProducts(): Promise<Product[]> {
    const products = await this.productService.getProducts();
    return products;
  }

  @Get('/:productId')
  public async getProduct(@Param('productId') productId: number) {
    const product = await this.productService.getProduct(productId);
    return product;
  }

  @Patch('/edit/:productId')
  public async editProduct(
    @Body() createProductDto: CreateProductDTO,
    @Param('productId') productId: number,
  ): Promise<Product> {
    const product = await this.productService.editProduct(
      productId,
      createProductDto,
    );
    return product;
  }

  @Delete('/delete/:productId')
  public async deleteProduct(@Param('productId') productId: number) {
    const deletedProduct = await this.productService.deleteProduct(productId);
    return deletedProduct;
  }
}

In the file above, we imported the necessary modules to handle the HTTP requests and proceeded to inject the ProductService that was created earlier into the controller through the constructor to make use of the functions that are already defined within ProductService. Next, we created the following asynchronous methods:

  • createProduct(): This method will be used to process a POST HTTP request sent from the client-side to create a new product and persist it in the database.
  • getProducts(): This method will be responsible for fetching the entire list of products from the database.
  • getProduct(): This method takes the productId as a parameter and uses it to retrieve the details of the product with that unique productId from the database.
  • editProduct(): This method is used for editing the details of a particular product.
  • deleteProduct(): This method also accepts the unique productId to identify a particular product and delete it from the database.

Another important thing to note here is that each of the asynchronous methods we defined has a metadata decorator as the HTTP verb. They take in a prefix that Nest.js uses to further identify and point to the method that should process a request and respond accordingly.

For instance, the ProductController that we created has a prefix of product and a method named createProduct() that takes in the prefix create. This means that any GET request directed to product/create (http://localhost:3000/product/create) will be handled by the createProduct() method. This process is also the same for other methods defined within this ProductController.

Updating the product module

Now that the controller and service have been created and automatically added to the ProductModule with the use of the nest command, we need to update the ProductModule. Open ./src/product/product.module.ts and update its content with the following code:

import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductRepository } from './product.repository';

@Module({
  imports: [TypeOrmModule.forFeature([ProductRepository])], // add this
  controllers: [ProductController],
  providers: [ProductService],
})

export class ProductModule {}

Here, we passed the ProductRepository class to the TypeOrm.forFeature() method. This will now enable the usage of the ProductRepository class.

The application is now ready and we can run it to test all the endpoints created so far. Run the following from the terminal:

npm run start:dev

This will start the application on http://localhost:3000. At this point, you can use a tool like Postman to test the API. Postman is a testing tool used to confirm and check the behavior of your API before deploying to production.

Using the application

Create a product

Create product

Here, we send a POST HTTP request to http://localhost:3000/product/create endpoint with the name, description, and price of a product. The created product was returned as a response.

Get products

Get products

A GET HTTP request call was made to http://localhost:3000/product/all to retrieve the entire list of products created.

Get product

Get product

To retrieve the details of a single product, we sent a GET HTTP request to the http://localhost:3000/product/2 endpoint. Please note that 2 is the unique productId of the product that we are interested in. Feel free to try other values.

Edit a product

Edit product

Here, a PATCH HTTP request was sent to http://localhost:3000/product/edit/2 endpoint and we updated the details of the product identified with the productId of 2.

Writing tests for the application

Now that our API is working as expected, in this section, we’ll focus on writing tests for the methods defined in the ProductService class that was created earlier. It feels appropriate to only test this part of the application as it handles most of the business logic.

Nest.js comes with a built-in testing infrastructure, which means we don’t have to set up a lot of configuration in terms of testing. Though Nest.js is agnostic to testing tools, it provides integration with Jest out of the box. Jest will provide assert functions and test-double utilities that help with mocking.

Currently, the product.service.spec.ts file has the following code.

import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';

describe('ProductService', () => {
  let service: ProductService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ProductService],
    }).compile();
    service = module.get<ProductService>(ProductService);
  });

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

We will add more tests to make this fully cover all the methods defined within the ProductService.

Writing a test for the ‘create’ and ‘get’ products methods

Remember, we did not start this project using the test-driven development approach. So, we’ll write the tests to ensure that all business logics within the ProductService receives the appropriate parameters and return the expected response. To start, open the product.service.spec.ts file and replace its content with the following:

import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
import { ProductRepository } from './product.repository';
import { NotFoundException } from '@nestjs/common';

describe('ProductService', () => {
  let productService;
  let productRepository;
  const mockProductRepository = () => ({
    createProduct: jest.fn(),
    find: jest.fn(),
    findOne: jest.fn(),
    delete: jest.fn(),
  });

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ProductService,
        {
          provide: ProductRepository,
          useFactory: mockProductRepository,
        },
      ],
    }).compile();
    productService = await module.get<ProductService>(ProductService);
    productRepository = await module.get<ProductRepository>(ProductRepository);
  });

  describe('createProduct', () => {
    it('should save a product in the database', async () => {
      productRepository.createProduct.mockResolvedValue('someProduct');
      expect(productRepository.createProduct).not.toHaveBeenCalled();
      const createProductDto = {
        name: 'sample name',
        description: 'sample description',
        price: 'sample price',
      };
      const result = await productService.createProduct(createProductDto);
      expect(productRepository.createProduct).toHaveBeenCalledWith(
        createProductDto,
      );
      expect(result).toEqual('someProduct');
    });
  });

  describe('getProducts', () => {
    it('should get all products', async () => {
      productRepository.find.mockResolvedValue('someProducts');
      expect(productRepository.find).not.toHaveBeenCalled();
      const result = await productService.getProducts();
      expect(productRepository.find).toHaveBeenCalled();
      expect(result).toEqual('someProducts');
    });
  });
});

First, we imported the Test and TestingModule packages from @nestjs/testing module. This provides the method createTestingModule, which creates a testing module that will act as the module defined earlier within the test. In this testingModule the providers array is composed of ProductService and a mockProductRepository to mock the custom ProductRepository using a factory.

We then created two different components of the test suite to ensure that we can create a product and retrieve the lists of products.

Let’s add a couple more scripts to test the functionality of retrieving and deleting a single product within the application. Still within the product.service.spec.ts file, update it by adding the following code just below our existing test script:

import { Test, TestingModule } from '@nestjs/testing';
import { ProductService } from './product.service';
import { ProductRepository } from './product.repository';
import { NotFoundException } from '@nestjs/common';

describe('ProductService', () => {
  ...
  describe('getProduct', () => {
    it('should retrieve a product with an ID', async () => {
      const mockProduct = {
        name: 'Test name',
        description: 'Test description',
        price: 'Test price',
      };
      productRepository.findOne.mockResolvedValue(mockProduct);
      const result = await productService.getProduct(1);
      expect(result).toEqual(mockProduct);
      expect(productRepository.findOne).toHaveBeenCalledWith(1);
    });

    it('throws an error as a product is not found', () => {
      productRepository.findOne.mockResolvedValue(null);
      expect(productService.getProduct(1)).rejects.toThrow(NotFoundException);
    });
  });

  describe('deleteProduct', () => {
    it('should delete product', async () => {
      productRepository.delete.mockResolvedValue(1);
      expect(productRepository.delete).not.toHaveBeenCalled();
      await productService.deleteProduct(1);
      expect(productRepository.delete).toHaveBeenCalledWith(1);
    });
  });
});

To get a particular product, we simply created a mockProduct with some default details and verified that we can retrieve and delete a product.

Check here on GitHub for the complete test script.

Running the test locally

Before running the test, you should delete the test file created for the AppController located in src/app.controller.spec.ts, you can create this manually later if you wish to write a test for it. Now, go ahead and run the test with:

npm run test

You will see the following output.

 PASS  src/product/product.service.spec.ts
  ProductService
    createProduct
      ✓ should save a product in the database (11ms)
    getProducts
      ✓ should get all products (2ms)
    getProduct
      ✓ should retrieve a product with an ID (2ms)
      ✓ throws an error as a product is not found (4ms)
    deleteProduct
      ✓ should delete product (2ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.951s, estimated 3s
Ran all test suites.

Test result terminal

Automating the tests

At this point, we have fully built a complete working RESTful API with Nest.js and wrote the tests for its business logic. The next thing to do is to add the configuration file to set up continuous integration with CircleCI. This will help ensure that a subsequent update on our code will not break any existing functionality as our tests will run automatically once we push to a GitHub repository. The repository will be created in the next section. To begin, create a folder called .circleci and create a new file named config.yml within it. Open the new file and paste the following code into it:

version: 2.1
orbs:
  node: circleci/node@3.0.0
jobs:
  build-and-test:
    executor:
      name: node/default
    steps:
      - checkout
      - node/install-packages
      - run:
          command: npm run test
workflows:
  build-and-test:
    jobs:
      - build-and-test

Here, we specified the version of CircleCI to use and used CircleCI Node orb to set up and install Node.js. We then proceeded to install all the dependencies for the project. The final command is the actual test command, which runs our test.

Create a GitHub repository

To facilitate the continuous integration process, navigate to GitHub, and create a new repository with a similar name to your project, in this case, nest-starter-testing.

Repository page

Next, initialize Git locally by running this command from the root directory of your project:

git init

Now commit all your changes, add the repository, and push the project to GitHub with:

git add .
git commit -m "Initial commit"
git remote add origin https://github.com/<your_username>/<repo_name> (In my case `https://github.com/yemiwebby/nest-starter-testing`)
git push -u origin master

Once this process is complete, you will have your local repository linked to your remote repository on GitHub.

Add project

Create an account on CircleCI by navigating to this page. Next, if you’re part of any organization, you will need to select the organization you wish to work under to set up your repository with CircleCI.

Once you’re on the project page, find the project that we created on GitHub earlier and click Set Up Project.

Project page

This will show you a configuration page with the details from the config.yml file that was created earlier. Now click Start Building.

Start building page

Follow the prompt and click Add Manually, since we included the configuration file already.

Add manually page

Click Start Building on the next prompt.

Start building prompt

You will be taken to the pipelines page where you can view your newly running build.

Pipeline page

You will see your pipeline start to run automatically and pass!

Build page

You can also click on the workflow to see the details of the build.

Test result page

This is great. All the tests ran successfully and showed outputs similar to what we had when the test was run locally. Subsequently, all you have to do is add more features to your project, write more tests, and push to GitHub. The continuous integration pipeline will automatically run and the tests will be executed.

Conclusion

Nest.js encourages and enforces excellent structure for web applications. It helps in organizing things and following best practices. In this tutorial, we learned how to build RESTful APIs with Nest.js and test functionality using Postman. Finally, we wrote a couple of tests and automated them using CircleCI.

We focused mainly on testing the ProductService. The knowledge gained from here while writing that test suite can be easily extended to other parts of the application if you would like to explore further.

The complete source code for this application is here on GitHub.


Oluyemi is a tech enthusiast, programming freak, and a web development junkie who loves to embrace new technology.