Features

  • Typescript
  • Eslint
  • Prettier
  • Husky
  • Environment
  • Testing setup
  • Error handling
  • Logger
  • Rate limiter
  • Security against xss
  • Load testing
  • Using Sequalize ORM

Body Parser

npm i body-parser
import bodyParser from 'body-parser';
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Environment Variables

npm i dotenv

Create a .env file and put something inside it

APPLICATION_PORT = 4000
import 'dotenv/config';

console.log(process.env.APPLICATION_PORT);

or like this

import dotenv from 'dotenv';
const configuration: any = dotenv.config().parsed;

console.log(configuration.APPLICATION_PORT);

Multiple Environment

Install cross-env so that we don't face issues loading the environment from windows shells

yarn add -D cross-env

Create files according to environment like .env.development and .env.production

Then create separate scripts for those inside package.json

"dev": "cross-env NODE_ENV=development ts-node-dev --respawn src/index.ts",
"prod": "cross-env NODE_ENV=production ts-node-dev --respawn src/index.ts",

Then modify the config file like this:

import dotenv from 'dotenv';
dotenv.config({ path: __dirname + `/./../../.env.${process.env.NODE_ENV}` });

const config = {
  port: process.env.APPLICATION_PORT,
};

export default config;

That's it

Add Husky

yarn add -D husky

Then add the following commands inside the package.json

"husky": {
    "hooks": {
      "pre-commit": "yarn lint"
    }
}

Logging

There are several libraries for logging but the best of them are winston

yarn add winston

Then create a logger for logging

import { format, transports, createLogger } from 'winston';

const logger = createLogger({
  format: format.combine(format.timestamp(), format.json()),
  transports: [new transports.Console(), new transports.File({ level: 'error', filename: 'errors.log' })],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      format: format.combine(format.colorize(), format.simple()),
    }),
  );
}

export default logger;

Cool thing about winston is we can use multiple transports which means we can give output to different systems. For example in the above example we are giving error logs to a file named errors.log but all other levels are going to be in the console

we can control the format of the logging by using format. Which is available in logform library

We are clorizing the development logging at the end Then use the logger by doing something like this

import logger from './utils/logger';

logger.error('test');

We can also add context or service level information for better understanding.

const logger = createLogger({
  defaultMeta: {
    service: 'billing-service',
  },
  //... other configs
});

In this case a service field will also be added to the log object.

Sometimes we need individual log level control. For example if we want to track the flow of a user we may need to add that info for each level of that information. That is not possible with service level customization

For this purpose we can use child-logger

This concept allows us to inject context information about individual log entries.

import logger from './utils/logger';

const childLogger = logger.child({ requestId: '451' });
childLogger.error('test');

We can also log exceptions and unhandled promise rejections in the event of a failure. winston provides us with a nice tool for that. We can

const logger = createLogger({
  transports: [new transports.File({ filename: 'file.log' })],
  exceptionHandlers: [new transports.File({ filename: 'exceptions.log' })],
  rejectionHandlers: [new transports.File({ filename: 'rejections.log' })],
});

We can profile our requests in a simple way

app.get('/ping/', (req: Request, res: Response) => {
  console.log(req.body);
  logger.profile('meaningful-name');
  // do something
  logger.profile('meaningful-name');
  res.send('pong');
});

This will give an output of additional output about the performance

{ "durationMs": 5, "level": "info", "message": "meaningful-name", "timestamp": "2022-03-12T17:40:59.093Z" }

You can see more examples with winston here

Using Morgan

This far you should understand why winston is one of the best if not the best logging library. But it's used for general purpose logging. There is another library that can help us with more sophisticated logging especially for http requests. That library is called morgan

First create a middleware that will intercept all the requests. I am adding it inside middlewares/morgan.ts file.

import morgan, { StreamOptions } from 'morgan';

import Logger from '../utils/logger';

// Override the stream method by telling
// Morgan to use our custom logger instead of the console.log.
const stream: StreamOptions = {
  write: (message) => Logger.http(message),
};

const skip = () => {
  const env = process.env.NODE_ENV || 'development';
  return env !== 'development';
};

const morganMiddleware = morgan(':method :url :status :res[content-length] - :response-time ms :remote-addr', {
  stream,
  skip,
});

export default morganMiddleware;

Notice how we modified our stream method to use the winston logger. There are some predefined log formats for morgan like tiny and combined You can use those like the following

const morganMiddleware = morgan('combined', {
  stream,
  skip,
});

This will give output in a separate format

Now use this middleware inside out index.ts file.

import morganMiddleware from './middlewares/morgan';

app.use(morganMiddleware);

Now all out requests will be logged inside the winston with http level

{ "level": "http", "message": "GET /ping 304 - - 11.140 ms ::1\n", "timestamp": "2022-03-12T19:57:43.166Z" }

We can create a separate log file for http logs by updating out winston logger a bit. The final version can look something like this

import { format, transports, createLogger } from 'winston';

const { combine, timestamp, json, align } = format;
const errorFilter = format((info, opts) => {
  return info.level === 'error' ? info : false;
});

const infoFilter = format((info, opts) => {
  return info.level === 'info' ? info : false;
});

const httpFilter = format((info, opts) => {
  return info.level === 'http' ? info : false;
});

const logger = createLogger({
  format: combine(
    timestamp(),
    json(),
    format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
  ),
  transports: [
    new transports.Console(),
    new transports.File({
      level: 'http',
      filename: 'logs/http.log',
      format: format.combine(httpFilter(), format.timestamp(), json()),
    }),
    new transports.File({
      level: 'info',
      filename: 'logs/info.log',
      format: format.combine(infoFilter(), format.timestamp(), json()),
    }),
    new transports.File({
      level: 'error',
      filename: 'logs/errors.log',
      format: format.combine(errorFilter(), format.timestamp(), json()),
    }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      format: format.combine(format.colorize(), format.simple()),
    }),
  );
}
export default logger;

Now in a production system maintaining these log files can be a pain in the a**. That's why there is a nice module named winston-daily-rotate-file

We can use this to configure in such a way so that our log files rotate daily

First install it

yarn add winston-daily-rotate-file

Then replace our transports inside the winston

const infoTransport: DailyRotateFile = new DailyRotateFile({
  filename: 'logs/info-%DATE%.log',
  datePattern: 'HH-DD-MM-YYYY',
  zippedArchive: true,
  maxSize: '20m',
  maxFiles: '14d',
  level: 'info',
  format: format.combine(infoFilter(), format.timestamp(), json()),
});

do this for all the log levels and pass it inside the transports in winston

transports: [new transports.Console(), httpTransport, infoTransport, errorTransport],

Now you will see new log files inside the logs folder named in the format we specified.

That should take care of all your logging problems.

Testing

We will use jest for testing. First install it

yarn add -D jest @types/jest

Then add a command in package.json for test

"scripts": {
    "test": "jest"
},

Now create a sample test. The convention is creating it inside __tests__ folder

test('Multiplying two numbers', async () => {
  expect(1).toBe(1);
});

Then run the command

yarn test

It will show you all the passing tests

Now let's create a coverage for the tests as well. Add another command inside package.json

"scripts": {
    "test": "jest",
    "test-coverage": "jest --coverage"
},

And if you run

yarn test-coverage

It will generate the test coverage report for you

Compression

Compression is a technique that can reduce the size of the static file and json response In nodejs that can be done with a nice middleware package named compression

First install it

yarn add compression

Then add it inside your index.ts

import compression from 'compression';
app.use(compression());

And that's it! There are other options that you can use. Refer to the documentation for that

Security

We will first use another nice npm package named helmet to define some of the http headers for us and provides some basic security features out of the box!

yarn add helmet

Then use it inside your index.ts

import helmet from 'helmet';

app.use(helmet());

You should also take a look at helmet-csp

Prevent DOS attack

DOS means Denial of Service. If an attacker tries to swamp your server with requests then our real users can feel the pain of slow response time.

To prevent this we can use a nice package named toobusy-js This will monitor the event loop and we can define a lag parameter which will monitor the lag of the event loop and indicate if our event loop is too busy to serve requests right now.

yarn add toobusy-js

Then add a new middleware to indicate that the server is too busy right now.

import toobusy from 'toobusy-js';

app.use(function (req, res, next) {
  if (toobusy()) {
    res.send(503, 'Server too busy!');
  } else {
    next();
  }
});

Rate Limiting

Rate limiting helps your application from brute-force attacks. This helps to prevent the server from being throttled.

Unauthorized users can perform any number of requests with malicious intent and you can control that with rate-limiting. For example you can allow a user to make 5 request per 15 minutes for creating account. Or you can allow unsubscribed users to make requests at a certain rate limit. something like 100requests/day

There is a nice package named express-rate-limit. First install it

yarn add express-rate-limit

Then create a rate limiting configuration for it.

import rateLimit from 'express-rate-limit';

export const rateLimiter = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 24 hrs in milliseconds
  max: 100, // maximum number of request inside a window
  message: 'You have exceeded the 100 requests in 24 hrs limit!', // the message when they exceed limit
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

const app = express();

app.use(rateLimiter);

This will allow you to add rate limit for all of your routes. You can also just add rate-limiting for specific routes.

But if you are behind a proxy. Which is the case for most cases when you use any cloud provider like heroku aws etc then the IP of the request is basically the ip of the proxy which makes it look like that request is coming from a single source and the server gets clogged up pretty quick.

To resolve this issue you can find out the numberOfProxies between you and the server and set that count right after you create the express application

const numberOfProxies = 1;
const app = express();

app.set('trust proxy', numberOfProxies);

To learn more about trust proxy refer to the documentation

Configure Cors

CORS will keep your application safe from malicious attacks from unknown sources It's really easy to configure in nodejs

npm i cors

then use it inside the index.ts file

import cors from 'cors';

let corsOptions = {
  origin: 'http://example.com',
  optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
};
app.use(cors());

Prevent XSS attacks

XSS attack means cross-site scripting attacks. It injects malicious scripts into your application.

An attacker can use XSS to send a malicious script to an unsuspecting user. The end user’s browser has no way to know that the script should not be trusted, and will execute the script. Because it thinks the script came from a trusted source, the malicious script can access any cookies, session tokens, or other sensitive information retained by the browser and used with that site.

You can protect your application by using xss-clean

yarn add xss-clean

Then use it inside the index.ts file

import xss from 'xss-clean';

app.use(xss());

Prevent SQL Query injection attacks

If you use Sequalize, TypeORM these type of orm tools then you are safe by default because these help us against the SQL query injection attacks by default

Limit the size of the body of the request

Using body-parser you can set the limit on the size of the payload

npm i body-parser

By default body-parser is configured to allow 100kb payloads size. You can set the limit like the following

import bodyParser from 'body-parser';
app.use(bodyParser.json({ limit: '50kb' }));
app.use(bodyParser.urlencoded({ extended: true }));

Use linter

A linter can force you to follow these best practices by default. You can use eslint-plugin-security for that.

yarn add -D eslint-plugin-security

And inside your .eslintrc file

"extends": ["plugin:@typescript-eslint/recommended", "plugin:security/recommended"],

Enforce HTTPS

You should always use HTTPS over HTTP when possible.

yarn add hsts

Then use it inside your index.ts

import hsts from 'hsts';

app.use(
  hsts({
    maxAge: 15552000, // 180 days in seconds
  }),
);

Use CSRF Protection middleware

To learn more about CSRF. Go here COnsider using csurf

import csrf from 'csurf';
var csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, function (req, res) {
  // generate and pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() });
});

Some more resource:

https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html https://medium.com/@nodepractices/were-under-attack-23-node-js-security-best-practices-e33c146cb87d

Docker Support

Follow the following article for more information

https://www.mohammadfaisal.dev/blog/express-typescript-docker

Graceful Shutdown

You can do that by using the following code

const server = app.listen(config.port, () => {
  console.log(`listening at ${config.port}`);
});

process.on('SIGTERM', function () {
  server.close(function () {
    process.exit(0);
  });
});