Building REST API With Express
Building REST API With Express
and Swagger
I’ve started working with JS in 2017, since then I am writing frontend and backend code with it.
It is easy to write web-server with NodeJS and I never found any serious performance issue in
using NodeJS. According to Stack Overflow 2020 survey, NodeJS is the most popular
technology. I prefer using Express with NodeJS. It is one of the most popular [Link] web
application frameworks. There are multiple frameworks, and you can choose whichever you
want according to the need.
After working with TypeScript, it became my preferred language of choice between JS and TS.
TypeScript is the superset of JavaScript, means all valid JS is valid TypeScript. So it is easy to
learn Typescript if you already knew JavaScript. TypeScript is 2nd most loved language
according to the Stack Overflow 2020 survey. TypeScript helps you to add static types to the
Javascript code. It is very helpful in writing, maintaining, and debugging code.
Bootstrap project
Let's create a directory with your preferred application name and set up an empty node project
inside it. You can choose to customize [Link] or accepts all of the default options by
passing -y flag to init command.
mkdir express-typescript
cd express-typescript
npm init -y
npm i -D typescript
Add [Link] in the root of the project directory. Here we define outDir as ./build to
put generated JavaScript files. You can put your preferred directory name. You can customize
the config file more as per your need. Check TypeScript Handbook for more details.
[Link]
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./build",
"strict": true,
"esModuleInterop": true
}
}
Install Express as dependency and type definitions of node and express as development
dependencies.
npm i -S express
npm i -D @types/express @types/node
This code will run the express server, listening to port 8000. It will add /ping route, which will
reply JSON response on the GET call.
src/[Link]
[Link](PORT, () => {
[Link]("Server is running on port", PORT);
});
Let's add the build command. it will transpile the TypeScript code into JavaScript and put the
generated code in the output directory as mentioned in [Link].
[Link]
"scripts": {
"build": "tsc",
}
Now let's build the JavaScript code with the build command.
npm run build
After running the above command we can see the JS code generated in the build folder. Now
with node, we can run the server. We can visit [Link] to see the JSON
response.
node build/[Link]
Now add the dev script to [Link], which will run the nodemon command. Add nodemon
config to [Link]. We can keep the config as a separate file. But I prefer to add it to
[Link] to keep the root of the project clean. Here we are configuring nodemon to watch all
the .ts files inside the src folder and execute ts-node src/[Link] on any code change.
[Link]
"scripts": {
"build": "tsc",
"dev": "nodemon",
},
"nodemonConfig": {
"watch": [
"src"
],
"ext": "ts",
"exec": "ts-node src/[Link]"
}
After running the dev command, we can see the nodemon is running. And the server is up and
running as well.
Add middlewares
Let's extend the server by adding some middlewares. We are going to add three middleware to
the server. [Link] is built-in middleware to parse the request body, [Link] is
also built-in middleware used to serve the static files, and morgan is used to logs the requests.
Let's install them as dependencies and their type definitions as development dependencies in the
project.
npm i -S morgan
npm i -D @types/morgan
After installing the middleware, we can use them in the code. We will add them to the server
with [Link]() function. Here we make the public folder to serve the static files.
src/[Link]
[Link]([Link]());
[Link](morgan("tiny"));
[Link]([Link]("public"));
Now after running the server, open [Link] in the browser. We can see the
request gets logged in the terminal.
Refactor
Till now the server is one single file. It is okay for small servers, but it is difficult to extend the
server if it is one file. So we will create multiple files.
Let's create a controller for the ping request in src/controllers/[Link] path. Here we add a
class called PingController with method getMessage, we define the response interface with a
property message as a string.
src/controllers/[Link]
interface PingResponse {
message: string;
}
Now create a sub router in src/routes/[Link] file and move all the routing login there. In
the server, we will add this sub router as a middleware.
src/routes/[Link]
src/[Link]
[Link]([Link]());
[Link](morgan("tiny"));
[Link]([Link]("public"));
[Link](Router);
[Link](PORT, () => {
[Link]("Server is running on port", PORT);
});
Swagger integration
Let's add OpenAPI documentation with the Swagger. We need to add tsoa to generates a JSON
file with OpenAPI Specifications for all the APIs. We also need swagger-ui-express to host
the Swagger JSON with Swagger UI.
npm i -S tsoa swagger-ui-express
npm i -D @types/swagger-ui-express concurrently
[Link]
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
We need to create the config file for tsoa. Add [Link] at the root of the directory. Add
entryFile and outputDirectory in the config. Here we are setting public as the output folder
for the generated JSON file.
[Link]
{
"entryFile": "src/[Link]",
"noImplicitAdditionalProperties": "throw-on-extras",
"spec": {
"outputDirectory": "public",
"specVersion": 3
}
}
We update the dev and build command to generate Swagger docs. We add tsoa spec to
generate Swagger docs. We will be running the swagger command before build and dev
command with prebuild and predev Respectively. We add concurrently to the dev
command, which will run the nodemon and tsoa spec on parallel. The Swagger docs will get
auto-updated on every code change during development.
[Link]
"scripts": {
"start": "node build/[Link]",
"predev": "npm run swagger",
"prebuild": "npm run swagger",
"build": "tsc",
"dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec\"",
"swagger": "tsoa spec",
},
Let's update the server file to serve the Swagger UI. We add swagger-ui-express to serve the
Swagger UI for the hosted swagger JSON file.
src/[Link]
import express, { Application, Request, Response } from "express";
import morgan from "morgan";
import swaggerUi from "swagger-ui-express";
[Link]([Link]());
[Link](morgan("tiny"));
[Link]([Link]("public"));
[Link](
"/docs",
[Link],
[Link](undefined, {
swaggerOptions: {
url: "/[Link]",
},
})
);
[Link](Router);
Now let's update the controller and add decorators to the class and methods to define the path
and route for the API documentation. tsoa will pick the return type PingResponse as the
response type for the /ping route.
src/controllers/[Link]
interface PingResponse {
message: string;
}
@Route("ping")
export default class PingController {
@Get("/")
public async getMessage(): Promise<PingResponse> {
return {
message: "pong",
};
}
}
After making all the changes and running the server, visit [Link] to access
the APIs documentation.
Additional Resources
Building a [Link]/TypeScript REST API, Part 1: [Link]
Next
Why Docker.
Docker helps organizations to ship and develop applications better and faster. It will be easy to
set up the development environment on any new machine with docker as it abstracts out lots of
complexity of setting up dependencies and environment. Docker also isolates the project from
other projects in the same machine so the developer can run multiple projects without having any
conflict with the required dependencies.
Docker makes it easy to configure and setup dependencies and environments for the application.
As most of the companies have dedicated teams to do setup and manage infrastructure, Docker
gives more power to developers to configure without depending on other teams to do the setup.
Write Dockerfile.
To Dockerize the server, we need to create a Dockerfile. A Dockerfile is just a list of
instructions to create a docker image. Read more about Dockerfile here
Each line in the Dockerfile is a command and create a new image layer of its own. Docker
caches the images during the build, so every rebuild will only create the new layer which got
changed from the last build. Here the order of commands is very significant as it helps to reduce
the build time.
Let's start writing Dockerfile for the server. Here we are taking node:12 as the base image for
the server docker image. Explore dockerhub for more node image version. Here we are copying
the [Link] and doing npm install first, then copying the other files. Docker will cache
the images of these two steps during the build and reuse them later as they change less
frequently. Here we will be running the development server with the docker image, so we need
to give npm run dev as the executing command.
Dockerfile
FROM node:12
WORKDIR /app
COPY package*.json ./
COPY . .
EXPOSE 8000
We need to add .dockerignore to tell docker build to ignore some files during the COPY
Command.
.dockerignore
node_modules
[Link]
After creating the Dockerfile, we need to run the docker build to create a docker image from
the Dockerfile. Here we are naming the docker image as express-ts
We can verify the docker image by running the docker images command. Here we can see the
name, size, and tag of the docker images.
docker images
REPOSITORY TAG IMAGE ID
CREATED SIZE
express-ts latest d0ce1e38958b 2
minutes ago 1.11GB
We can run the docker image with the docker run command. Here we can mapping the system
port 8000 to docker container port 8000. We can verify if the server is running or not by visiting
[Link]
We will add the [Link] file to the root of the project to mount the local src
folder. Read more about docker-compose here
[Link]
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./src:/app/src
ports:
- "8000:8000"
We need to run the command docker-compose up to start the server. Now the server is running
in development mode with auto-restart on code changes. We can verify the server is restarting on
code changes by making any code change in the TypeScript files.
docker-compose up
The docker setup for the development server is completed. Let's rename the Dockerfile as
[Link] and update the [Link] file. We will use the Dockerfile for the
production image, which we are going to set up in the next section.
mv Dockerfile [Link]
[Link]
version: "3"
services:
app:
build:
context: .
dockerfile: [Link]
volumes:
- ./src:/app/src
ports:
- "8000:8000"
Add Production Dockerfile
Let's start building a docker image for the production server. We need to create a new Dockerfile
and add the following commands. Here after copying the files, we need to build the JavaSript
files and execute the npm start command.
FROM node:12
WORKDIR /app
COPY package*.json ./
COPY . .
EXPOSE 8000
After running the docker build command, we can see the docker image is created for the
production server.
docker images
REPOSITORY TAG IMAGE ID
CREATED SIZE
express-ts latest d0ce1e38958b 2
minutes ago 1.11GB
Here the image size is 1.11GB, which is not optimized. Let's optimize the docker image and
reduce the size.
First, instead of taking node:12 as the base image, we will be taking its alpine variant. Alpine
Linux is very lightweight. Read more about alpine-docker here.
FROM node:12-alpine
Let's build the docker image with the updated Dockerfile. Here we are tagging the docker image
as alpine so we can compare the image size with the previous build.
There are still some issues with our docker image as development dependencies are there in
production build and TypeScript code is there, which is not required while running the server in
production. So let's optimize the docker image further with a multi-stage build.
Here we create two stages, one for building the server and the other for running the server. In the
builder stage, we generate Javascript code from the Typescript files. Then in the server stage, we
copy the generated files from the builder stage to the server stage. In the Server stage, we need
only production dependencies, that's why we will pass the --production flag to the npm
install command.
Let's build the docker image with the updated multi-staged Dockerfile. Here we are tagging the
docker image as ms so we can compare the image sizes with the previous builds.
After running the docker images command we can see the difference in the sizes of docker
images. The multi staged image is the leanest among all images.
We have dockerized the development and production version of the Express and TypeScript
REST API server.
Building REST API with Express, TypeScript - Part 3: PostgreSQL and Typeorm
Building REST API with Express, TypeScript - Part 4: Jest and unit testing
In this post, we will be extending the REST API server by setting up the database connection and
adding some APIs. We will be using PostgreSQL for the database. You can choose any database
as per requirements. In this sample app, there will be few entities that will have relations with
each other. That's why we are using a relational database for the server.
There are many ways to handle the database from the app. Some people prefer to connect
database engine directly and make raw SQL Queries. And some people prefer to use ORM to
connect and query the database engine. There are some advantages and tradeoffs of using ORM.
Like ORM provides an abstraction over raw queries, which speeds up the development. But
sometimes for some complex operations, ORM queries tend to be slow, in those conditions it is
better to write raw queries.
There are multiple ORMs for Nodejs like Sequelize, Bookshelf, and TypeORM. We will be
using TypeORM because it has better TypeScript support at this time.
In the [Link] file, we are adding a database service named db using postgres
images and adding it as a dependency to the app service.
[Link]
version: "3"
services:
db:
image: postgres:12
environment:
- POSTGRES_DB=express-ts
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
app:
build:
context: .
dockerfile: [Link]
volumes:
- ./src:/app/src
ports:
- "8000:8000"
depends_on:
- db
environment:
- POSTGRES_DB=express-ts
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_HOST=db
Here we are using a database called express-ts and using postgres as username and password
to connect the database from the REST server. Let's build and start the services.
docker-compose build
docker-compose up
src/config/[Link]
Import the database configuration in the [Link] file and pass the config to the TypeORM's
createConnection function. The createConnection function is asynchronous, so we will wait
for the promise to resolve before binding and listening to the PORT. If there is an error in
connecting to the database we will log the error and exit the server.
src/[Link]
import "reflect-metadata";
import { createConnection } from "typeorm";
import express, { Application } from "express";
import morgan from "morgan";
import swaggerUi from "swagger-ui-express";
[Link]([Link]());
[Link](morgan("tiny"));
[Link]([Link]("public"));
[Link](
"/docs",
[Link],
[Link](undefined, {
swaggerOptions: {
url: "/[Link]",
},
})
);
[Link](Router);
createConnection(dbConfig)
.then((_connection) => {
[Link](PORT, () => {
[Link]("Server is running on port", PORT);
});
})
.catch((err) => {
[Link]("Unable to connect to db", err);
[Link](1);
});
Creating Models
Let's create models for the REST Server. We will have 3 models in the server - User, Post, and
Comment. We will put them in the models directory.
Let's create the User model first. Here we need to add the Entity decorator to the User class.
The User model will create the user table in the database with id, firstName, lastName,
email, createdAt, and updatedAt as table columns. The id is the primary key of the user table
and will be auto-generated with an auto-increment value. The createdAt & updatedAt fields
will be auto-generated too and set during insert and update operations.
src/models/[Link]
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
firstName!: string;
@Column()
lastName!: string;
@Column()
email!: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
Here we are adding an exclamation mark (!) to the properties because the properties are not
assigned in the constructor and can have the value undefined. We can either set
strictPropertyInitialization to true in the [Link] file or add the exclamation mark.
Adding an exclamation mark in the model properties is better than changing the config for the
whole project.
src/models/[Link]
We have to import the User model in the database config file and add it to the entities
property.
src/config/[Link]
We will do the same thing for other models, ie. Post and Comment. We will create the Post and
Comment models in the models directory and define columns to the models. In the Post models
we will have id, title, content, userId, createdAt, and updatedAt columns. We are passing
type as text to the Column decorator for the content column to explicitly define the column
type as text.
The Post model is dependent on the User model as every post will be created by some user. So
the column userId will map to the user. Here the userId is the foreign key for the user table.
We also add a relation to the user model. We are defining a property called user of type User
and adding relations ManyToOne as one user can create multiple posts.
src/models/[Link]
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
JoinColumn,
} from "typeorm";
import { Comment } from "./comment";
import { User } from "./user";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column({
type: "text",
})
content!: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
As the Post model, the Comment model is also dependent on the User model, so we will add the
userId column as a foreign key and add the user property with the type User and with the
relation ManyToOne with the User model.
The Comment model is also dependent on the Post model as there can be multiple comments on
a post that's why a comment will be mapped to a post. The column postId will be a foreign key
to the post table. we also add the property called post of type Post. We also have to add a
property called comments of type array of Comment to the Post model.
src/models/[Link]
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
JoinColumn,
} from "typeorm";
import { Post } from "./post";
import { User } from "./user";
@Entity()
export class Comment {
@PrimaryGeneratedColumn()
id!: number;
@Column({
type: "text",
})
content!: string;
@Column({ nullable: true })
userId!: number;
@ManyToOne((_type) => User, (user: User) => [Link])
@JoinColumn()
user!: User;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
As both the Post and the Comment models are dependent on the User model and have relations
ManyToOne to it. We have to add the posts and comments properties to the User model with the
relation OneToMany as one user can have multiple posts and comments. These properties will be
of type array of Post and array of Comment respectively.
src/models/[Link]
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany,
UpdateDateColumn,
} from "typeorm";
import { Post } from "./post";
import { Comment } from "./comment";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
firstName!: string;
@Column()
lastName!: string;
@Column()
email!: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
src/models/[Link]
We have to import the Post and the Comment models in the database config file and add them to
the entities property as we did with the User model.
src/config/[Link]
After a successful login to the psql cli, we can run the commands to select the database and then
list all the tables and get the tables definitions.
\l # list database.
\c express-ts # select `express-ts` database.
\dt # list all tables.
\d user # Show `user` table definition.
\d post # Show `post` table definition.
\d comment # Show `comment` table definition.
User Apis
Let's add the APIs for the user. We will create 3 APIs - Create user, Fetch user, and Fetch all
users. We will be following the data mapper pattern. Each model will have one repository. And
from the server, we will query the database through the repository only. We will import the
models in the app just for the type. There will be no direct operation through the models. This
pattern helps in maintainability in large scale projects.
Let's create the user repository in the repositories directory. We will be exporting 3 functions
from this file. The getUsers function is simple it just calls the find method on user repository
and return list of User. All the 3 functions are asynchronous and return a Promise as the return
type. The createUser function requires an argument called payload. We are defining the type
of it as the interface IUserPayload. In the function, we will create a new instance of the User
model and call the save method on the user repository. This will insert the row in the user table.
In the getUser function we call the findOne method on the user repository. If the value is
undefined we will return null otherwise return the found user.
src/repositories/[Link]
Let's add a controller for the user APIs. We will create a class called UserController and add
the Route and the Tags decorator to it. This is for swagger file generation. Here we define 3
methods - getUsers, createUser, and getUser. Then we add the decorators to them. The
createUser method is the post request, and the schema of the body is of the IUserPayload
type.
src/controllers/[Link]
@Route("users")
@Tags("User")
export default class UserController {
@Get("/")
public async getUsers(): Promise<Array<User>> {
return getUsers();
}
@Post("/")
public async createUser(@Body() body: IUserPayload): Promise<User> {
return createUser(body);
}
@Get("/:id")
public async getUser(@Path() id: string): Promise<User | null> {
return getUser(Number(id));
}
}
Let's create a router for the user APIs. We need to create a [Link] file in the routes
directory. In this file, we will create a new express router and add all the user routes to it. We
will export the router from the file and import it into the root router and use it for the /users
route.
src/routes/[Link]
src/routes/[Link]
[Link]("/users", UserRouter);
After adding the routes and restarting the server, we can verify the APIs by checking the swagger
docs. The swagger docs will be updated with the User APIs. We can call the APIs directly from
the swagger docs or call them from the terminal. We can create a user with some random data
and then call the GET API to get the user.
curl [Link]
curl [Link]
src/repositories/[Link]
src/repositories/[Link]
We will create controllers for both the Post and the Comment APIs. We will create
[Link] and [Link] files in the controller directory and
create classes called PostController and CommentController in them respectively.
src/controllers/[Link]
import { Get, Route, Tags, Post as PostMethod, Body, Path } from "tsoa";
import { Post } from "../models";
import {
createPost,
getPosts,
IPostPayload,
getPost,
} from "../repositories/[Link]";
@Route("posts")
@Tags("Post")
export default class PostController {
@Get("/")
public async getPosts(): Promise<Array<Post>> {
return getPosts();
}
@PostMethod("/")
public async createPost(@Body() body: IPostPayload): Promise<Post> {
return createPost(body);
}
@Get("/:id")
public async getPost(@Path() id: string): Promise<Post | null> {
return getPost(Number(id));
}
}
In the Post Controller, there is a conflict with the Post model name and Post decorator. That's
why we are aliasing the Post decorator as PostMethod in this file.
src/controllers/[Link]
@Route("comments")
@Tags("Comment")
export default class CommentController {
@Get("/")
public async getComments(): Promise<Array<Comment>> {
return getComments();
}
@Post("/")
public async createComment(@Body() body: ICommentPayload): Promise<Comment>
{
return createComment(body);
}
@Get("/:id")
public async getComment(@Path() id: string): Promise<Comment | null> {
return getComment(Number(id));
}
}
We will create the routers for both the post and the comment APIs. We will import both routers
in the root router file and use it for the /posts and /comments route.
src/routes/[Link]
src/routes/[Link]
src/routes/[Link]
[Link]("/users", UserRouter);
[Link]("/posts", PostRouter);
[Link]("/comments", CommentRouter);
After restarting the server, the swagger docs will be updated and will have post and comment
APIs.
All the source code for this tutorial is available on GitHub.
Next
Building REST API with Express, TypeScript - Part 4: Jest and unit testing
Why to test ?
Testing is an important part of the software development life cycle. It is a process of verifying
the behavior of the application. It validates that the application is working as it is intended to be
and meets all the requirements.
The codebase grows as we introduce more features to the application. It is difficult and not
efficient to verify all features manually before every release. Thus writing tests to verify the
behavior of the application is easier and more efficient. We can just run the tests after making
any changes to verify that the application is working as expected.
Testing helps to discover bugs early in the development lifecycle and increases developer
confidence in releasing any new changes.
There are multiple types of software testing. In this blog, we will focus mainly on unit testing.
Unit Testing
In unit testing, we test the unit of the application in isolation. A unit can be anything like class,
function, method, etc. Unit tests require minimum effort to set up and write tests. Unit tests also
act as a self-documentation for the function/method. It also helps in debugging as the developer
can focus on one part of the application instead of the whole app.
Setup Jest
There are multiple frameworks for unit testing JavaScript code. We will be using Jest which is
currently the most popular testing framework.
Add test script in [Link]. In test script call the jest command.
[Link]
"scripts": {
...
"test": "jest"
},
Let's add a dummy test to check if the Jest setup so far is working or not.
src/controllers/[Link]
After running the test command the test should fail. Change the false to true and re-run the
test again. It should pass. The Jest setup so far is working fine.
Let's add a test for the ping controller. The getMessage method has to return pong. We will
verify this in the test.
src/controllers/[Link]
After running the test command, the test will fail as we are importing a typescript file in the
test. We need to transpile it. Let's install ts-jest as a dev dependency and create the Jest
configuration file by ts-jest's config:init command and re-run the test again after this.
npm i -D ts-jest
npx ts-jest config:init
npm test
The ts-jest's config:init command will add the [Link] file to the root of the project
and after running the command the test will pass. The Jest setup is done, we can add some tests
for the server.
Add Tests
Let's start with the user controller. Create the [Link] file in the
controllers folder next to the [Link] file. We will group the tests with
describe block.
Here we will be mocking the UserRepository as in unit testing it is better to test just the unit
(function/method) and mock the dependencies. Mocking the dependencies gives more control as
now the developer can mock the behavior of the dependencies and can test multiple edge cases
without messing up with the actual dependency. Like here will change the return value of the
[Link] method as our test cases without setting up the Database.
src/controllers/[Link]
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await [Link]();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
[Link]();
});
});
});
In spying, we are replacing the original method with a mock function and change its
implementation to return the value as per our need. This will help developers to recreate and test
most of the edge cases which will difficult to do in manual testing. After running the test we are
restoring the spy function to the original.
Let's test the condition where the list is not empty. The method is supposed to return the list as it
is. We create a dummy list data and change the implementation of the
[Link] method to return the dummy list data. We can verify by just
comparing the actual output and expected output.
src/controllers/[Link]
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await [Link]();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
[Link]();
});
Let's add a library to create dummy data for the test. We will use faker to generate fake data and
structure the fake data as actual data in the test's utilities. We will create a test folder in the root
directory where we keep all the utilities and setup files.
npm i -D faker @types/faker
test/utils/[Link]
The generateUserData function creates the fake user database object and generateUsersData
will just return the list of it. We can override these fake objects by just passing the extra params.
Let's give the test folder an absolute path so we can easily import the utilities anywhere in the
tests. We also have to exclude the test files from the build files as it is not needed to include the
test files in the production server code.
[Link]
{
"compilerOptions": {
...
"baseUrl": "./",
"paths": {
"test/*": ["test/*"]
}
},
"include": ["src/**/*"],
"exclude" : ["src/**/*.[Link]"]
}
[Link]
[Link] = {
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"test/(.*)": "<rootDir>/test/$1",
},
};
Let's replace the dummy list data with the generateUsers fake data. and instead of calling the
[Link] after every test. We will call the [Link] in afterEach which
will reset all the mock after every test.
src/controllers/[Link]
afterEach(() => {
[Link]();
});
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await [Link]();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
Let's add tests for the UserController's addUser and getUser method. For the addUser method
tests we need the utility method to generate the fake data for the test. The tests are simple here as
we don't have much logic in the controllers. We just have to mock the repository methods as per
the test case and verify the output of the controller methods with the expected result.
We will also verify the number of calls and arguments for the spy functions. This is not needed
but it is recommended to have this check.
test/utils/[Link]
...
src/controllers/[Link]
afterEach(() => {
[Link]();
});
describe("UserController", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(UserRepository, "getUsers")
.mockResolvedValueOnce([]);
const controller = new UserController();
const users = await [Link]();
expect(users).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
describe("addUser", () => {
test("should add user to the database", async () => {
const payload = generateUserPayload();
const userData = generateUserData(payload);
const spy = jest
.spyOn(UserRepository, "createUser")
.mockResolvedValueOnce(userData);
const controller = new UserController();
const user = await [Link](payload);
expect(user).toMatchObject(payload);
expect(user).toEqual(userData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
Let's add tests for the Post and Comment controllers. We will follow the same process as we did
in the user controller tests. Right now the logic is the same in the controllers. It is easier to just
copy-paste the test code and update the values & variables names as per the controller.
First, we need to add the utility functions to generate the fake data for the post-controller tests.
we will add the functions the same utils file test/utils/[Link]
test/utils/[Link]
import faker from "faker";
import { User } from "../../src/models";
...
src/controllers/[Link]
afterEach(() => {
[Link]();
});
describe("PostController", () => {
describe("getPosts", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(PostRepository, "getPosts")
.mockResolvedValueOnce([]);
const controller = new PostController();
const posts = await [Link]();
expect(posts).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
describe("createPost", () => {
test("should add post to the database", async () => {
const payload = generatePostPayload();
const postData = generatePostData(payload);
const spy = jest
.spyOn(PostRepository, "createPost")
.mockResolvedValueOnce(postData);
const controller = new PostController();
const post = await [Link](payload);
expect(post).toMatchObject(payload);
expect(post).toEqual(postData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("getPost", () => {
test("should return post from the database", async () => {
const id = 1;
const postData = generatePostData({ id });
const spy = jest
.spyOn(PostRepository, "getPost")
.mockResolvedValueOnce(postData);
const controller = new PostController();
const post = await [Link]([Link]());
expect(post).toEqual(postData);
expect(post?.id).toBe(id);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
test("should return null if post not found", async () => {
const id = 1;
const spy = jest
.spyOn(PostRepository, "getPost")
.mockResolvedValueOnce(null);
const controller = new PostController();
const post = await [Link]([Link]());
expect(post).toBeNull();
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
We can verify the post controllers tests just by runnings the npm test command. We will add
the utility functions to generate the fake data for the comment controller tests in the same test's
utilities file.
test/utils/[Link]
...
The logic of the test code for the comment controller will be the same as the user and post
controller test code.
src/controllers/[Link]
afterEach(() => {
[Link]();
});
describe("CommentController", () => {
describe("getComments", () => {
test("should return empty array", async () => {
const spy = jest
.spyOn(CommentRepository, "getComments")
.mockResolvedValueOnce([]);
const controller = new CommentController();
const comments = await [Link]();
expect(comments).toEqual([]);
expect(spy).toHaveBeenCalledWith();
expect(spy).toHaveBeenCalledTimes(1);
});
describe("createComment", () => {
test("should add comment to the database", async () => {
const payload = generateCommentPayload();
const commentData = generateCommentData(payload);
const spy = jest
.spyOn(CommentRepository, "createComment")
.mockResolvedValueOnce(commentData);
const controller = new CommentController();
const comment = await [Link](payload);
expect(comment).toMatchObject(payload);
expect(comment).toEqual(commentData);
expect(spy).toHaveBeenCalledWith(payload);
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe("getComment", () => {
test("should return comment from the database", async () => {
const id = 1;
const commentData = generateCommentData({ id });
const spy = jest
.spyOn(CommentRepository, "getComment")
.mockResolvedValueOnce(commentData);
const controller = new CommentController();
const comment = await [Link]([Link]());
expect(comment).toEqual(commentData);
expect(comment?.id).toBe(id);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveBeenCalledTimes(1);
});
Coverage
Let's collect coverage for the tests. The coverage tells how much source code is covered by the
tests.
Jest support collecting coverage out of the box and doesn't need any other library. We just need
to pass the coverage options to the jest cli.
It is better to enable the coverage in the Jest config file instead of passing the argument via the
cli. By default Jest collect the coverage of only those files which are included in the test cases.
We can pass the glob pattern of the source code files to override this behavior and collect the
coverage from all the files.
[Link]
[Link] = {
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"test/(.*)": "<rootDir>/test/$1",
},
collectCoverage: true,
collectCoverageFrom: ["src/**/*.{js,ts}"],
};
After running the coverage we can see that the controller files have good coverage but other files
are not covered yet. Let's add some more tests to increase the coverage.
More Tests
Let's add tests for the repository files. The repositories have a dependency on a third-party
library i.e. "typeorm". The typeorm helps to connect to the database. As in unit tests, it is ideal to
not have any external dependencies like a database as well as any third-party libraries. So we
will be mocking the typeorm then we can easily test the repository logic without setting up the
database.
Let's add tests for the user repository. we will create a [Link] file next to
the [Link] file inside the repositories directory.
Here we are mocking the whole module, not just a method as we did in the controller's tests. We
are mocking the getRepository method which returns the mocked find. In the getUsers tests,
we will mock the implementation of the find method as per test case requirements. In
beforeEach we will clear the mock implementation of the find method.
src/repositories/[Link]
[Link]("typeorm", () => {
return {
getRepository: [Link]().mockReturnValue({
find: [Link](),
}),
PrimaryGeneratedColumn: [Link](),
Column: [Link](),
Entity: [Link](),
ManyToOne: [Link](),
OneToMany: [Link](),
JoinColumn: [Link](),
CreateDateColumn: [Link](),
UpdateDateColumn: [Link](),
};
});
const mockedGetRepo = mocked(getRepository(<[Link]>{}));
beforeEach(() => {
[Link]();
});
describe("UserRepository", () => {
describe("getUsers", () => {
test("should return empty array", async () => {
[Link]([]);
const users = await [Link]();
expect(users).toEqual([]);
expect([Link]).toHaveBeenCalledWith();
expect([Link]).toHaveBeenCalledTimes(1);
});
For the addUser method tests we will mock the implementation of the save method and will
clear the mock in beforeEach.
src/repositories/[Link]
[Link]('typeorm', () => {
return {
getRepository: [Link]().mockReturnValue({
find: [Link](),
save: [Link]()
}),
...
}});
describe("UserRepository", () => {
...
describe("addUser", () => {
test("should add user to the database", async () => {
const payload = generateUserPayload()
const userData = generateUserData(payload)
[Link](userData)
const user = await [Link](payload);
expect(user).toMatchObject(payload)
expect(user).toEqual(userData)
expect([Link]).toHaveBeenCalledWith(payload)
expect([Link]).toHaveBeenCalledTimes(1)
})
})
})
We will mock the implementation of the findOne method for the getUser tests and clear its
implementation in beforeEach as we did with save and find methods.
src/repositories/[Link]
[Link]('typeorm', () => {
return {
getRepository: [Link]().mockReturnValue({
find: [Link](),
save: [Link](),
findOne: [Link]()
}),
...
}});
describe("UserRepository", () => {
...
describe("getUser", () => {
test("should return user from the database", async () => {
const id = 1
const userData = generateUserData({id})
[Link](userData)
const user = await [Link](id)
expect(user).toEqual(userData)
expect(user?.id).toBe(id)
expect([Link]).toHaveBeenCalledWith({id})
expect([Link]).toHaveBeenCalledTimes(1)
})
test("should return null if user not found", async () => {
const id = 1
[Link](null)
const user = await [Link](id)
expect(user).toBeNull()
expect([Link]).toHaveBeenCalledWith({id})
expect([Link]).toHaveBeenCalledTimes(1)
})
})
})
Let's move the mock implementation of the typeorm module from the
[Link] file to a separate file. Then we can easily import and use it in other
repository tests.
We will create a __mocks__ folder in the root directory and add create a [Link] file in it.
We will move the typeorm mock implementation to this file. The __mocks__ is a special folder
and Jest will pick the mock from here. You can read more about it here.
__mocks__/[Link]
[Link] = {
getRepository: [Link]().mockReturnValue({
find: [Link](),
save: [Link](),
findOne: [Link](),
}),
PrimaryGeneratedColumn: [Link](),
Column: [Link](),
Entity: [Link](),
ManyToOne: [Link](),
OneToMany: [Link](),
JoinColumn: [Link](),
CreateDateColumn: [Link](),
UpdateDateColumn: [Link](),
};
src/repositories/[Link]
[Link]("typeorm");
We will follow the same process and write the tests for the [Link] and
[Link] same as the user repository.
CI Setup
Let's setup Github Actions to run tests. We will set up it to run tests on push and pull_request
events. We will run the npm ci command to install dependencies and the npm test command to
run the tests. We will set up GitHub Actions to run the tests on multiple node versions.
.github/workflows/[Link]
name: [Link] CI
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
steps:
- uses: actions/checkout@v2
- name: Use [Link] ${{ [Link]-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ [Link]-version }}
- run: npm ci
- run: npm test
Additional Links
1. Testing Javascript with Kent C. Dodds
2. Jest
3. ts-jest