Adding Typescript to an ExpressJS Rest API with Jest & Supertest

Tom Lamb
5 min readOct 3, 2021

--

Because javascript is a dynamically typed language, as a developer you are never sure what will happen until you actually run your code. As your codebase grows or maybe as more people join your new revolutionary SaaS startup, it will be more and more difficult to keep track of all the variables and their types which might introduce bugs into your app.

Enter Typescript, which is a superset of javascript developed and adds optional static typing to javascript.

What does this mean? If configured to do so, Typescript will make your code typed and therefore will force you to think about what type of data is passed to functions, classes or variables.

Important: This article follows the previous article “Rest API with Express router, Jest and Supertest”. To follow this tutorial before adding Typescript, you can click here. To skip the tutorial, you can also find the GitHub repository here.

Introduction

In the previous article, we set up an Express server for our Rest API and added Jest and Supertest to be able to test it. In this article we are going to build on this by adding Typescript.

Before getting started, this being quite a big change to the repository, it is a good idea to start a new branch, so let’s do this with the following command:

git checkout -b 'adding-typescript'

In this new branch let’s start by adding some dev dependencies that will allow us to set up Typescript:

npm i -D typescript ts-node @types/node @types/express tsconfig-paths

Typescript needs a configuration file to know which features of Typescript you would like to use in your repository. For this there is a very handy command:

npx tsc --init

This will create a tsconfig.json file which should have all the possible configuration options with most of them commented out.

It is recommended to have a look through the Typescript documentation to understand this json file and the different options available to you. You can find the documentation here.

In the meantime, here are the options we will be using for our API.

{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"baseUrl": "./",
"paths": {
"@src/*": ["./src/*"]
},
"esModuleInterop": true,
"inlineSources": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": false
}
}

Notice how we added a paths option here where@src/* will resolve to ./src/*. This means that wherever we are in our repository we can use @src/... to refer to the src folder without having to thing about relative paths.

For this to work though we have to configure Nodemon and Jest which we will do a little later. For now though, let’s change all our .js files to .ts files.

At this point, there might be a few files that turn red! Don’t worry we will sort this out.

Writing Typescript

index.ts

import dotenv from 'dotenv'; 
import app from './app';

dotenv.config();
const port = process.env.PORT || 8080;app.listen(port,
() => console.log(`App is listening on http://localhost:${port}`)
);

app.ts

In our app.ts file, we will have to ensure that all the arguments passed to our middleware callback are of the right type. For this we can import Request, Response and NextFunction from express.

import express, { Request, Response, NextFunction} from 'express';import api from '@src/routes';const app = express(); app.use(express.json());app.use('/api', api);export interface CustomError extends Error { 
statusCode?: number;
}
app.use((_req: Request, _res: Response, next: NextFunction) => {
const error: CustomError = new Error('Not found');
error.statusCode = 404;
next(error);
});
app.use((err: CustomError, _: Request, res: Response) => {
const statusCode = err.statusCode || 500;
const name = err.name || 'Error';
res
.status(statusCode)
.json({ name, message: err.message });
});
export default app;

In our Javascript app we could add a statusCode to our error object without any issue. In Typescript though, the Error object doesn’t have a property statusCode therefore Typescript will complain if we try to add it.

We therefore need to tell Typescript that the Error object we receive or create here is a different type that has all the properties of the Error object plus statusCode.

src/routes/index.ts

import express from "express"; 
import photos from "./photos";
const router = express.Router(); router.use("/photos", photos); export default router;

src/routes/photos/index.ts

import express, { Request, Response, NextFunction } from "express";const router = express.Router();  router.get("", (_req: Request, res: Response, _next: NextFunction) => { 
res.json({
message: "hello world"
})
});
export default router;

package.json

In our package.json we need to add some nodemon configuration so that it understands some of our Typescript configuration. We also need to change some of our scripts.

"nodemonConfig": { 
"ignore":
[
"**/*.test.ts",
"**/*.spec.ts",
".git",
"node_modules"
],
"watch": [
"src",
"index.ts",
"app.ts"
],
"exec": "node -r tsconfig-paths/register -r ts-node/register index.ts",
"ext": "ts, js"
},
"scripts": {
"test": "jest --watch --verbose",
"dev": "nodemon",
"build": "tsc"
},

Important: We also need to remove the following code from package.json. Don’t worry if you don’t have this code in your package.json, this is only if you are following along from the previous article.

"jest": { 
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
]
},

Testing

In order for the testing to work in Typescript we will need to add a few modules.

npm install -D ts-jest @types/jest @types/supertest

In the end of the previous section, we removed the jest configuration from our package.json so that now we can add the configuration in its own file by running the following command. You may skip this step if you already have a jest.config.js file.

npx ts-jest config:init

jest.config.js

const { pathsToModuleNameMapper } = require("ts-jest/utils"); 
const { compilerOptions } = require("./tsconfig.json");
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coveragePathIgnorePatterns: ["/node_modules/"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), };

src/tests/index.test.ts

import request from "supertest";
import app from "../../app"
describe('Sanity test', () => {
test('1 should equal 1', () => {
expect(1).toBe(1)
})
})
describe('Photos endpoint', () => {
test('should return Hello World', async () => {
const res = await request(app)
.get('/api/photos')
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
message: "hello world"
})
})
})

Now we can run our tests to see if everything is still ok.

npm run test

Conclusion

Typescript can often be a headache to set up and there are a lot of different options that can be added to make your code even stricter. The great thing about Typescript is that you can incrementally add stricter options bit by bit.

You can find the boilerplate on GitHub here.

I hope you liked this tutorial! Please don’t hesitate to leave any comments you might have!

--

--

Tom Lamb

Full Stack Javascript developer, skateboarder, skier and tech enthusiast! Visit my website at https://tom-lamb.com