Levelling up an Express API, Part 3

  developmentexpressjs

This is the final post in my 3-part series on “levelling up” an Express API to make it production-readier. Part 1 touched on the basics - version control, TypeScript, linting, testing - and part 2 added secrets & configuration, deployment considerations, and caching.

I took the approach of building on existing simple code, rather than creating a ready-to-go starter, as I like to better understand any dependencies I add to my code. Any of the “level ups” I’ve described can be used in isolation, or even skipped.

The rest of this post continues production-ready tips, and assumes Express 4.17 (circa mid 2019), Typescript and a main file server.ts.

The complete project is at https://github.com/thomasswilliams/pedals-api-nov-2019.

Level 7: Rate limiting

A properly configured Express API can serve thousands of requests per second. Rate limiting helps by restricting one client’s ability to make too many requests (“spamming”). Spamming could be caused intentionally/maliciously or by accidental misconfiguration. There’s even a HTTP status to inform clients that they’re being rate limited (HTTP status 429).

I was once involved in a real world example where rate limiting could have saved $$$. A browser kept re-sending a request to an API, and the API sent a text message to a mobile phone. The fix seemed simple enough - find the user and politely ask them to stop. When that wasn’t immediately possible, an outage was necessary to add rate limiting to the API.

I’m using NPM package express-rate-limit - first, I add to my project at a command prompt:

npm install --save express-rate-limit
npm install --save-dev @types/express-rate-limit

This adds express-rate-limit and typings for TypeScript completion in an editor like VS Code. Then in my server.ts file, I add:

// rate limiter for Express routes https://www.npmjs.com/package/express-rate-limit
import rateLimiter from 'express-rate-limit';

other code

// prior to defining routes, wire up rate limiter for all requests
app.enable('trust proxy');

// create new rate limiter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const limiter = new (rateLimiter as any)({
  // timeframe is 3 seconds
  windowMs: 3 * 1000,
  // limit each IP to 5 requests per timeframe
  max: 5
});
// apply rate limiter to all requests
// also needs to be applied per route, before cache middleware
app.use(limiter);

// route: get pedals collection
// rate limit route using default settings
// cache this route for 5 minutes
app.get('/pedals', limiter, cache, (req, res) => {
  console.log('Got pedals collection');
  // hard-code JSON response
  res.send({ pedals: [{ name: 'Boss SY-1', id: 1 }] });
});

In production Express APIs, I also rate limit my login route as well as my catch-all 404 route, and add rate limiting tests to my test script.

This is a good time to note that the order of Express middleware matters. I chose to put the rate limiter before caching. If it was the other way around, the cached response would be sent before rate limiting.

Level 8: Security headers

Security can be a touchy topic for developers. It’s necessary to consider, yet difficult to add later in the project life, and recommended practice seems to be regularly changing.

While I won’t cover every aspect of security, I production-ready my Express APIs with Cross-Origin Resource Sharing AKA CORS and a great multi-purpose HTTP header solution called helmet. Note CORS may need configuration on the client, which is unfortunately out of scope for this blog post.

First I add cors and helmet and typings from a command prompt:

npm install --save cors helmet
npm install --save-dev @types/cors @types/helmet

Then in my server.ts, I import them and configure (towards the top of the file, before defining routes):

// CORS HTTP headers https://www.npmjs.com/package/cors
import cors from 'cors';
// common security HTTP headers https://www.npmjs.com/package/helmet
import helmet from 'helmet';

other code

// simple usage for CORS
// more docs at https://www.npmjs.com/package/cors
app.use(cors());
// wire up all helmet HTTP headers
app.use(helmet());

// route: get pedals collection
// rate limit route using default settings
// cache this route for 5 minutes
// return CORS and other common security HTTP headers
app.get('/pedals', limiter, cache, cors(), helmet(), (req, res) => {
  console.log('Got pedals collection');
  // hard-code JSON response
  res.send({ pedals: [{ name: 'Boss SY-1', id: 1 }] });
});

Level 9: a couple of cosmetic touches

My last “level up” is a few cosmetic touches for my Express API:

  • ignore requests for “favicon”
  • increment package.json version each time I build for production
  • pre-build script to clean distribution directory

Ignore requests for “favicon”

I’ve added the code below to my Express APIs as some browsers request “favicon.ico”. There’s no impact on functionality - I put the following statements near the top of server.ts - just below where I declare the Express app:

// ignore requests for favicon adapted from https://stackoverflow.com/a/35408810/116288
// wire up earlier, before other middleware
app.get('/favicon.ico', (req, res) => {
  // return HTTP status 204 "No content" and end the response
  return res.sendStatus(204).end();
});

Increment package.json version on build

This is a “nice-to-have”, rather than must-have, step when building in the distribution directory (“dist”, can be configured in the tsconfig.json file).

First, I add a “build” script to package.json “scripts”:

build: tsc

Then I add a “postbuild” script, again in package.json “scripts”:

postbuild: npm version patch -s --no-git-tag-version

The NPM version command above increments the “patch” version number in ‘package.json’ by 1, without creating a Git version and tag.

More details of the NPM version command are at https://docs.npmjs.com/cli/version. My complete repo, including the “build” and “postbuild” scripts, is at https://github.com/thomasswilliams/pedals-api-nov-2019.

Pre-build script

Another minor improvement I’ve made when building my Express API for production is a pre-build script that clears the distribution directory. This uses the cross platform NPM package rimraf; first, add rimraf to the project from a command line:

npm install --save-dev rimraf

Then add a “prebuild” script to package.json “scripts”:

prebuild: rimraf dist"

Now when I run “npm run build”, previous builds are removed and I start with a clean distribution directory.

That wraps up my blog series on taking a “getting started” Express API and gradually “levelling up” to production readiness. Even though it’s not a complete list of tips it’s what I consider some of the biggest, easy wins. I hope it was useful!