Building REST API with Express, TypeScript 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 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.