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 -
npm install -D jest @types/jest ts-jest supertest @types/supertest
Now under the root of our project create a file jest.config.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
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 -
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 -
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
-
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 -
npm install cors express-rate-limit helmet hpp
npm install --save @types/cors @types/hpp
Now in your server.ts
under function addMiddlewares -
// 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 likexssFilter, 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 -
// 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 -
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
-
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 -
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 -
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 -
npm install module-alias
npm install --save-dev tsconfig-paths @types/module-alias
Now in the tsconfig.json
add the following -
"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 -
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
-
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 -
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 -
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. Until next time PEACE.
Top comments (0)