Rishabh Mishra

Building REST API with Express, TypeScript and Swagger

03 October 2020

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 Node.js 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.

What you will build

You will build REST API server with Express and TypeScript. It will generate production JavaScript code on build command. It will auto restart server on any code change during development, and it will auto generate OpenAPI documentation with Swagger.

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 package.json or accepts all of the default options by passing -y flag to init command.

mkdir express-typescript
cd express-typescript
npm init -y

Install Typescript as development dependency

npm i -D typescript

Add tsconfig.json 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.

tsconfig.json

{
  "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

Write server code

Let's add minimal code to make server up and running. Create a folder src inside the root folder. We will be going to put all the Typescript code inside it. It depends on personal choice. You can keep the code anywhere in the project.

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/index.ts

import express, { Application } from "express";

const PORT = process.env.PORT || 8000;

const app: Application = express();

app.get("/ping", async (_req, res) => {
  res.send({
    message: "pong",
  });
});

app.listen(PORT, () => {
  console.log("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 tsconfig.json.

package.json

"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 http://localhost:8000/ping to see the JSON response.

node build/index.js

Server is running on port 8000

Add development setup

The server is up and running. But still, development is difficult due to building and running the server manually after every code changes. It is better to automate this task. For this, we will use ts-node to run the typescript code directly, so then we don't have to run the typescript compiler during development. And to restart the ts-node on every code change, we will use nodemon which will watch the code and re-run the command on any changes.

Lets add ts-node nodemon as development dependencies in the project.

npm i -D ts-node nodemon

Now add the dev script to package.json, which will run the nodemon command. Add nodemon config to package.json. We can keep the config as a separate file. But I prefer to add it to package.json 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/index.ts on any code change.

package.json

  "scripts": {
    "build": "tsc",
    "dev": "nodemon",
  },

  "nodemonConfig": {
    "watch": [
      "src"
    ],
    "ext": "ts",
    "exec": "ts-node src/index.ts"
  }

After running the dev command, we can see the nodemon is running. And the server is up and running as well.

npm run dev

[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node src/index.ts`
Server is running on port 8000

Add middlewares

Let's extend the server by adding some middlewares. We are going to add three middleware to the server. express.json is built-in middleware to parse the request body, express.static 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 app.use() function. Here we make the public folder to serve the static files.

src/index.ts

import express, { Application } from "express";
import morgan from "morgan";

const PORT = process.env.PORT || 8000;

const app: Application = express();

app.use(express.json());
app.use(morgan("tiny"));
app.use(express.static("public"));

Now after running the server, open http://localhost:8000/ping in the browser. We can see the request gets logged in the terminal.

Server is running on port 8000
GET /ping 304 - - 2.224 ms

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/ping.ts 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/ping.ts

interface PingResponse {
  message: string;
}

export default class PingController {
  public async getMessage(): Promise<PingResponse> {
    return {
      message: "pong",
    };
  }
}

Now create a sub router in src/routes/index.ts file and move all the routing login there. In the server, we will add this sub router as a middleware.

src/routes/index.ts

import express from "express";
import PingController from "../controllers/ping";

const router = express.Router();

router.get("/ping", async (_req, res) => {
  const controller = new PingController();
  const response = await controller.getMessage();
  return res.send(response);
});

export default router;

src/index.ts

import express, { Application } from "express";
import morgan from "morgan";
import Router from "./routes";

const PORT = process.env.PORT || 8000;

const app: Application = express();

app.use(express.json());
app.use(morgan("tiny"));
app.use(express.static("public"));

app.use(Router);

app.listen(PORT, () => {
  console.log("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

We need to add support for Decorators in the tsconfig.json file.

tsconfig.json

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

We need to create the config file for tsoa. Add tsoa.json 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.

tsoa.json

{
  "entryFile": "src/index.ts",
  "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.

package.json

  "scripts": {
    "start": "node build/index.js",
    "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/index.ts

import express, { Application, Request, Response } from "express";
import morgan from "morgan";
import swaggerUi from "swagger-ui-express";

import Router from "./routes";

const PORT = process.env.PORT || 8000;

const app: Application = express();

app.use(express.json());
app.use(morgan("tiny"));
app.use(express.static("public"));

app.use(
  "/docs",
  swaggerUi.serve,
  swaggerUi.setup(undefined, {
    swaggerOptions: {
      url: "/swagger.json",
    },
  })
);

app.use(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/ping.ts

import { Get, Route } from "tsoa";

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 http://localhost:8000/docs/ to access the APIs documentation.

All the source code for this tutorial is available on GitHub.

Additional Resources

Next