This tutorial covers:
- Setting up and connecting a Nest.js app
- Creating a product using a Nest.js app
- Writing, running, and automating tests
Nest.js is a scalable and efficient server-side Node.js framework built with TypeScript. Nest.js was created to provide a structural design pattern to the Node.js development world. It was inspired by Angular.js and uses Express.js under the hood. Nest.js is compatible with the majority of Express.js middleware.
In this tutorial, I will lead you through building a RESTful API with Nest.js. The tutorial will familiarize you with the fundamental principles and building blocks of Nest.js. I will also demonstrate the recommended approach to writing tests for each API endpoint. I will wrap up the tutorial by showing you how to automate the testing process using CircleCI.
Prerequisites
There are a few things you will need for you to get the most out of this tutorial:
- Node version
v10.24.1
( lts/dubnium ) - MySQL version
>=8.0.20
- Nest CLI version
>=nestjs/cli@8.0.0
- A GitHub account
- A CircleCI account
- It is also helpful know a few things about TypeScript
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
The RESTful API that we build in this post will provision endpoints to create a product with a name, description, and price. We will edit, delete, and retrieve a single product, and also retrieve the entire list of products saved in the database.
This tutorial uses MySQL as the preferred relational database choice and combines it with TypeORM. Nest.js is database agnostic though, so you can choose to work with any database you prefer. You can find more details about databases and Nest.js here.
Setting up the Nest.js application
Run this 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. Select npm
and press the Enter key to start installing Nest.js. This process creates a new project in a nest-starter-testing
folder and installs 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 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.
Configuring and connecting Nest.js 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 need to install an accompanying package for it, along with a Node.js driver for MySQL. To do that, stop the app from running by pressing CTRL + C. Then run this command:
npm install --save @nestjs/typeorm typeorm mysql
When the installation process is complete, you can import the TypeOrmModule
into the root of the application.
Updating the TypeScript root module
The building blocks of Nest.js, modules are TypeScript files decorated with @Module
. Modules provide the metadata that Nest.js uses to organize the application structure. The root module in ./src/app.module.ts
is the top-level module. Nest.js recommends breaking 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 this 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.
We have established a connection with the database by importing TypeOrmModule
into the root AppModule
and specifying the connection options. These include the database details and the directory where the entity files will be stored. I will go into more detail about entity files in the next section.
Configure the database connection
In the prerequisites at the start of this tutorial, I referred to the MySQL download page. After you download, you will need to configure the database so that it works for this application.
In your terminal, log in to MySQL 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 preferable authentication for Node.js drivers for MySQL. To create the database, run:
CREATE DATABASE test_db;
Creating the product module, service, and controller for the Nest.js app
Now that you have 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 this 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, it is the product table.
Following the proper structure for a Nest.js app, create a new file within the src/product
folder and name it product.entity.ts
. Then paste this 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, when 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 specify shapes of objects extracted from the body of a request and provides a way to plug in validation easily.
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 this 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 this 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 thecreateProductDto
class, which will be used to extract the body of the HTTP request, as an argument. Then we destructurecreateProductDto
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 Nest.js 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 a product using this command:
nest generate service product
After running the command above, you will see this 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)
The nest
command has created two new files within the src/product
folder. These are:
- The
product.service.spec.ts
file will be used to write unit tests for the methods that will be created within the product service file. - The
product.service.ts
file holds all the business logic for the application.
The nest
command has also 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 and retrieving all products, as well as fetching, updating, and deleting the details of a particular product. Open the file and replace its content with this:
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:
- Create a new product:
createProduct()
- Get all created products:
getProducts()
- Retrieve the details of a single product:
getProduct()
- Edit the details of a particular product:
editProduct()
- Delete a single product:
deleteProduct()
It is important to note that we injected the ProductRepository
we created earlier into this service to easily interact and communicate with the database. Here is a snippet of the file showing this:
...
constructor(
@InjectRepository(ProductRepository)
private productRepository: ProductRepository,
) {}
...
This works only if we also import the ProductRepository
into the product module. We will do this later on in the tutorial.
Generating a Nest.js 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 this command from the terminal:
nest generate controller product --no-spec
You will see this output.
CREATE /src/product/product.controller.ts (103 bytes)
UPDATE /src/product/product.module.ts (261 bytes)
Because we will not 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 this:
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 this file, we imported the necessary modules to handle the HTTP requests and injected the ProductService
that was created earlier into the controller. That was done through the constructor to make use of the functions that are already defined within ProductService
. Next, we created these asynchronous methods:
- The
createProduct()
method is used to process a POST HTTP request sent from the client-side to create a new product and persist it in the database. - The
getProducts()
method fetches the entire list of products from the database. - The
getProduct()
method takes theproductId
as a parameter and uses it to retrieve the details of the product with that uniqueproductId
from the database. - The
editProduct()
method is used for editing the details of a particular product. - The
deleteProduct()
method also accepts the uniqueproductId
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 example, 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 this 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 this from the terminal:
npm run start:dev
This starts 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.
Creating a product using the Nest.js application
Create a POST HTTP request to http://localhost:3000/product/create
endpoint with the name
, description
, and price
of a product.
Get all products
Make a GET HTTP request call to http://localhost:3000/product/all
to retrieve the entire list of products created.
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. You can try other values too.
Edit a product
Send a PATCH HTTP request to http://localhost:3000/product/edit/2
endpoint and update the details of the product identified with the productId
of 2
.
Writing tests for the Nest.js 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 much 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 this 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 logic 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 this:
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 the @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 this 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
The output will be something like this:
> nest-starter-testing@0.0.1 test /Users/dominic/workspace/personal/circleci-gwp/nest-starter-testing
> jest
PASS src/app.controller.spec.ts
PASS src/product/product.service.spec.ts
Test Suites: 2 passed, 2 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 3.148 s
Ran all test suites.
Automating the tests
Now you have a fully built a RESTful API with Nest.js and tests for its business logic. Next, you will need to add the configuration file to set up continuous integration with CircleCI. Continuous integration helps ensure that an update on the code will not break any existing functionality. The tests will run automatically once it is pushed to a GitHub repository.
To begin, create a folder called .circleci
and in it, create a new file named config.yml
. Open the new file and paste this 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.
Setting up the project on CircleCI
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.
This will show you a configuration page that will allow you to select the CircleCI configuration file that you would like to use. This defaults to the configuration located at .circleci/config.yaml
on your main branch.
Now click Set Up Project.
Follow the prompt and click Add Manually, since we included the configuration file already. You will see your pipeline start to run automatically and pass.
This build had a single job: build-and-test
. All of the steps in the job are executed in a single unit, either within a fresh container, or a virtual machine.
You can also click on the Job to view the steps. Steps are collections of executable commands, which are run during a job
Clicking the steps shows further details. For example click the npm run test
step.
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.
Using what you have learned to build your own RESTful API with Nest.js
Nest.js encourages and enforces excellent structure for web applications. It helps your team organize work and follow 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.
In this tutorial, we focused on testing the ProductService
. You can apply the knowledge you gained 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 with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile. A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.