Setup express with Typescript - testing and security middleware
Tags: javascript, typescript, node
Published: 2022-07-12 07:05:30

### Introduction

In this series we will setup an express server using `Typescript`, we will be using `TypeOrm` as our ORM for querying a PostgresSql Database, we will also use `Jest` and `SuperTest` for testing. The goal of this series is not to create a full-fledged node backend but to setup an express starter project using typescript which can be used as a starting point if you want to develop a node backend using express and typescript.

### Overview

This series is not recommended for beginners some familiarity and experience working with `nodejs`, `express`, `typescript` and `typeorm` is expected. In this post which is part seven of our series we will : -
 - Set up jest and supertest.
 - Setup security middlewares.
 - Setup pino logging.
 - Setup import aliases (optional)

### Step One: Setup jest and supertest

In this section we will setup testing. This is how I do it. You might use some other configurations and might have other setups, please feel free to share your thoughts. In your terminal lets install the following dependencies - 
```bash
npm install -D jest @types/jest ts-jest supertest @types/supertest
```
Now under the root of our project create a file `jest.config.js`-
```js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node'
};
```
Under the root of our project create a new folder `tests` and under it create a new file `todos.test.ts`, and add a simple test
```ts
describe('test todos endpoint with db connection', () => {
  beforeAll(async () => {
    await AppDataSource.initialize();
  });

  afterAll(async () => {
    // await AppDataSource.createEntityManager().query(
    //   'truncate table todos cascade'
    // );
    await AppDataSource.destroy();
  });

  it('should fetch todos successfully', async () => {
    const response = await supertest(expressApp).get('/api/todos');
    expect(response.statusCode).toBe(200);
    expect(response.body).toHaveProperty('todos');
  });

  it('should create a todo successfully', async () => {
    const response = await supertest(expressApp).post('/api/todos').send({
      text: 'setup testing',
      status: 'done',
    });
    expect(response.statusCode).toBe(201);
    expect(response.body).toHaveProperty('todo');
  });
});
```

I hope you remember from our first part, that we had our express app initialized in `server.ts` and  under `app.ts` we started our server. And with this separation we can easily import our express app with all its routes in our tests. The above test is an `integration test` not an `unit test`. I prefer `integration testing` for backend apis, here we can test a lot of different scenarios with real-data and I don't have to mock a lot of stuff, like mocking middlewares, controllers, etc. 

In our integration test, as you might have noticed we are connecting to our database, when we run `jest` our `NODE_ENV` environment variable is set to `test` and we pick the test database configuration from our `src/config/dbConfig.ts` file. Which means we connect to our test database. In our test-db we should have our todos table, we will run our migrations against the test-db. One of the benefits of using migrations as opposed to typeorm's `synchronize: true` option. In your terminal run - 
```bash
NODE_ENV=test npm run migration:run
```
We can also programatically run typeorm's migrations in the beforeAll call - say for each endpoint, we can run migration for each table. You might have also noticed a truncate call in `afterAll`, this is very handy when you want to get rid of your testing data, but only when you are testing locally. If your tests run in a CI environment it might cause problems, when in your team multiple developers are running CI tests simultaneously. I personally use RDS, therefore I have a lambda that re-creates my test-db periodically. Lets test our function in terminal run - 
```bash
npx jest
```

But what if you want to run a `unit test` without this db connection, lets do it I will show you one unit test where we will mock our `service call`. I hope you remember from our previous tutorials, we discussed that we should have our database calls in separate service files as opposed to having them in our controller functions. It has a lot of benefits, one being testing them, mocking becomes very easy. In your `tests/todo.test.ts` - 
```ts
const getTodoByIdMock = jest
  .spyOn(TodosService.prototype, 'getTodoById')
  .mockImplementation(async () => {
    return {} as Todos;
  });

describe('test todos endpoint with mocks', () => {
  it('should fetch todo successfully', async () => {
    const response = await supertest(expressApp).get(
      '/api/todos/0d853566-fe0d-11ec-92c2-0214694f2400'
    );
    expect(response.statusCode).toBe(200);
    expect(response.body).toHaveProperty('todo');
    expect(getTodoByIdMock).toHaveBeenCalled();
  });
});
```
Given the fact that our services are classes I used jest's `spyOn` method and returned an empty todo. You can check jest's docs on how to mock a class method. Given that we are mocking our database call we don't need a database connection for our test suite. Here you can unit test the `getTodoById` function.

### Step Two: setup security middlewares
There are some middleware libraries that help secure our express server. First lets install all of them - 
```bash
npm install cors express-rate-limit helmet hpp
npm install --save @types/cors @types/hpp
```
Now in your `server.ts` under function addMiddlewares - 
```ts
  // configure middlewares for express
  private addMiddlewares() {
    // for parsing application/json
    this.app.use(express.json());
    // for parsing application/x-www-form-urlencoded
    this.app.use(express.urlencoded({ extended: true }));
    // add cors
    this.app.use(cors());
    // add security to the server using helmet middleware
    this.app.use(helmet());
    // protect against HTTP Parameter Pollution attacks
    this.app.use(hpp());
    // add rate limit to the whole server
    this.app.use(
      expressRateLimit({
        windowMs: 15 * 60 * 1000,
        max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
      })
    );
    this.app.set('trust proxy', 1);
  }
```
Let us understand what each middleware is doing - 
- We use `cors` for cross-origin, when we are calling our apis from a frontend.
- `helmet` helps us secure our express server, it adds some http security headers like `xssFilter, contentSecurityPolicy`, etc.
- `hpp` is used to avoid parameter pollution, check its docs.
- `expressRateLimit` is a very useful middleware. Lets say an attacker is sending 100 req/s to your server by running a script. This will slow down your server, make it go out of memory, your clients will see very large response times. To avoid this we use this middleware, basically we limit every Ip to a fixed number of connections over a period of time. Say every connection can only send 100 request to our server in a window of 15 minutes.

You might also notice we `this.app.set('trust proxy', 1)`, this is recommended by `express-rate-limiter` lib, for environments that have a server proxy. Like a load balancer sitting between the client and our server, in such a case all the request ips we receive are not the client ip but the load balancer's ip and we might end up limiting its requests, therefore we set `trust proxy` with the number of proxies which is 1 in my case. You can skip it if you don't have any proxies. To get the number of proxies we add a route to our server - 
```ts
 // configure routes for express
  private addRoutes() {
    // route to know the number of proxies
    this.app.get('/ip', (request, response) => response.send(request.ip));
    this.app.use('/api/todos', todosRouter);
  }
```
Go to `/ip` and see the IP address returned in the response. If it matches your IP address (which you can get by going to https://api.ipify.org/), then the number of proxies is correct and the rate limiter should now work correctly. If not, then keep increasing the number until it does.

### Step Three: Setup a logger

Loggers are important, when you deploy your application to say AWS EBS, EC2 all your logs are stored in cloudwatch. This comes in handy when you want to inspect your server logs in case of any error. In your terminal run the following - 
```bash
npm install pino pino-pretty express-pino-logger
npm install --save-dev @types/express-pino-logger
```
Under `src/utils` create a new file `logger.ts` - 
```ts
import pino from 'pino';

const levels = {
  http: 10,
  debug: 20,
  info: 30,
  warn: 40,
  error: 50,
  fatal: 60,
};

export const logger = pino({
  customLevels: levels,
  useOnlyCustomLevels: true,
  level: 'http',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      levelFirst: true,
      translateTime: 'yyyy-dd-mm, h:MM:ss TT',
    },
  },
});
```
Now under `server.ts` lets setup the logger middleware - 
```ts
import expressPinoLogger from 'express-pino-logger';
// configure middlewares for express
  private addMiddlewares() {
    // ...other middleware setup code
    // add logger middleware
    this.app.use(this.loggerSetup());
  }
  
  private loggerSetup() {
    return expressPinoLogger({
      logger: logger,
      autoLogging: false,
    });
  }
```
Now you can use the our logger, you need to import it first - 
```ts
private startServer() {
    this.server.listen(this.port, async () => {
      logger.info(`Server started on port ${this.port}`);
      try {
        await AppDataSource.initialize();
        logger.info('Database Connected');
      } catch (error) {
        logger.fatal(`Error connecting to Database - ${error}`);
      }
    });
 }
```

### Step Four: Add import aliases 

I like to setup import aliases for my projects. First install - 
```bash
npm install module-alias
npm install --save-dev tsconfig-paths @types/module-alias
```
Now in the `tsconfig.json` add the following - 
```json
"baseUrl": "./src",
"moduleResolution": "node",
 "paths": {
   "@api/*": ["api/*"],
   "@utils/*": ["utils/*"],
   "@middlewares/*": ["middlewares/*"],
   "@config/*": ["config/*"]
}
```
We add `baseUrl` to be "src", meaning all our aliases will be resolved from the src folder.

With the above setup you can start importing your files using aliases - 
```ts
import { asyncHandler } from '@middlewares/asyncHandler';
import { BaseRouter } from '@utils/BaseRouter';
import {
  validateRequestBody,
  validateRequestParams,
} from '@middlewares/validate';
```
But there is one caveat this will work in typescript but not in javascript, when we build our project javascript has not idea of `@middlewares/validate`. To use aliases in javascript we use `module-alias`. Create a new file under `src` called `path.ts` - 
```ts
import * as moduleAlias from 'module-alias';

moduleAlias.addAliases({
  '@api': `${__dirname}/api`,
  '@utils': `${__dirname}/utils`,
  '@middlewares': `${__dirname}/middlewares`,
  '@config': `${__dirname}/config`,
});
```
Now import this `path.ts` in your `app.ts` at the top - 
```ts
import 'dotenv/config';
import 'reflect-metadata';
import './path';
import * as http from 'http';
```
Now build your project - `npm run build` and run it using `npm run start` test all the endpoints it should work as expected.

Another small issue is our tests will fail, becuase jest has no idea of resolving  `@middlewares/validate`. So in your `jest.config.js` paste the following code - 
```js
const { pathsToModuleNameMapper } = require('ts-jest');

const { compilerOptions } = require('./tsconfig.json');

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: '<rootDir>/src',
  }),
};
```
Run your tests once to verify everything works as expected.

### Overview
In this tutorial we finished setting up tests and added some handy security middlewares. All the code for this tutorial can be found under the `feat/security-middleware` branch [here](https://github.com/yaldram/express-typescript-starter/tree/feat/security-middleware). Until next time PEACE.