DEV Community

loading...

lil-http-terminator, a tiny JS module to gracefully shutdown your HTTP server

Vasyl Boroviak
・4 min read

Increase your node_modules by 11 KB to get tranquility that your node.js HTTP server is shutting down without data loss risk.

Or how I shrunk a 2.2 MB module to 11 KB.

TL;DR: npm i lil-http-terminator

The problem

I've been coding node.js microservices for almost a decade now. Graceful HTTP server shutdown was always a problem I didn't wanna deal with because it's hard to get right.

The solution

http-terminator npm module

Recently I discovered that there is a perfect implementation of graceful shutdown. It's called http-terminator. Here is why I decided to use it (quoting the author):

  • it does not monkey-patch Node.js API
  • it immediately destroys all sockets without an attached HTTP request
  • it allows graceful timeout to sockets with ongoing HTTP requests
  • it properly handles HTTPS connections
  • it informs connections using keep-alive that server is shutting down by setting a connection: close header
  • it does not terminate the Node.js process

Usage:

import { createHttpTerminator } from 'http-terminator';
const httpTerminator = createHttpTerminator({ server });
await httpTerminator.terminate();
Enter fullscreen mode Exit fullscreen mode

Works with any node.js HTTP server out there (Express.js, Nest.js, Polka.js, Koa.js, Meteor.js, Sails.js, Hapi.js, etc).

Wow! Brilliant engineering! Well done author(s)!

But there is a catch.

Being a mere 4 KB codebase it adds 22 dependencies (2.2 MB, 464 files) to your node_modules.

See for yourself:

$ npx howfat -r tree http-terminator
npx: installed 18 in 1.695s

http-terminator@3.0.0 (22 deps, 2.16mb, 464 files)
├── delay@5.0.0 (10.91kb, 5 files)
├─┬ roarr@4.2.5 (19 deps, 2.02mb, 398 files)
│ ├── boolean@3.1.2 (7.9kb, 10 files)
│ ├── detect-node@2.1.0 (2.7kb, 6 files)
│ ├─┬ fast-json-stringify@2.7.7 (9 deps, 1.79mb, 268 files)
│ │ ├─┬ ajv@6.12.6 (5 deps, 1.41mb, 181 files)
│ │ │ ├── fast-deep-equal@3.1.3 (12.66kb, 11 files)
│ │ │ ├── fast-json-stable-stringify@2.1.0 (16.56kb, 18 files)
│ │ │ ├── json-schema-traverse@0.4.1 (19.11kb, 9 files)
│ │ │ ╰─┬ uri-js@4.4.1 (1 dep, 490.54kb, 51 files)
│ │ │   ╰── punycode@2.1.1 (31.67kb, 5 files)
│ │ ├── deepmerge@4.2.2 (29.39kb, 9 files)
│ │ ├── rfdc@1.3.0 (23.48kb, 9 files)
│ │ ╰── string-similarity@4.0.4 (10.73kb, 5 files)
│ ├─┬ fast-printf@1.6.6 (1 dep, 34.32kb, 26 files)
│ │ ╰── boolean@3.1.2 (🔗, 7.9kb, 10 files)
│ ├─┬ globalthis@1.0.2 (2 deps, 114.41kb, 41 files)
│ │ ╰─┬ define-properties@1.1.3 (1 dep, 48.41kb, 21 files)
│ │   ╰── object-keys@1.1.1 (25.92kb, 11 files)
│ ├── is-circular@1.0.2 (5.89kb, 8 files)
│ ├── json-stringify-safe@5.0.1 (12.42kb, 9 files)
│ ╰── semver-compare@1.0.0 (3.96kb, 8 files)
╰── type-fest@0.20.2 (108kb, 42 files)
Enter fullscreen mode Exit fullscreen mode

I got curious. What's that roarr package and if it can be removed from the package? The answer got me by surprise.

Removing the unnecessary dependencies

The three top level dependencies can be easily removed.

type-fest

The type-fest can be removed by rewriting the package from TS to JS. Hold saying your "boo" yet.

It's a single function module. You don't need the code completion for just one function. So, rewriting to JS shouldn't be a downside for TypeScript proponents.

delay

The delay module can be rewritten as a single-line function. Here it is:

const delay = time => new Promise(r => setTimeout(r, time));
Enter fullscreen mode Exit fullscreen mode

roarr

roarr module, the largest of the tree, takes 2 MB of your hard drive. But it is used literally in the single line!!!

if (terminating) {
  log.warn('already terminating HTTP server');

  return terminating;
}
Enter fullscreen mode Exit fullscreen mode

The module will print that warning in case you decide to terminate your HTTP server twice. That's all. There is no more usage of the roarr logger within the whole http-terminator module.

I find it nearly impossible to accidentally call .termiate() twice. It's hard to imagine this ever happens. So I decided to put the log variable to options and assign it to console by default.

We get rid of 20 dependencies and simultaneously allow you, my fellow developers, to customise the termination with the logger of your choice (winston, bunyan, pino, morgan, etc; or even the roarr itself).

Meet lil-http-terminator

I forked the http-terminator to lil-http-terminator.

const HttpTerminator = require("lil-http-terminator");
const httpTerminator = HttpTerminator({ server });
await httpTerminator.terminate();
Enter fullscreen mode Exit fullscreen mode

Being as awesome as the origin, the lil- version is:

  • 0 dependencies (original had 3 direct and 18 indirect sub-dependencies);
  • only 5 files (original was 464 files total);
  • only 11 KB (original was 2180 KB);
  • packaged by NPM as 3.9 KB .tar.gz file (original downloads about 522 KB).
  • takes much less memory (I didn't measure it though);
  • has 8 devDependencies (original has 17);

Afterword

I'm writing code for money for about 20 years. I'm using node.js and npm for almost a decade. I learnt to develop good and robust node.js services, scripts, serverless functions, apps. I discovered (re-invented) the best practices we better follow. I know how to make code maintainable years after it was written. The hardest bit was always the third party dependencies. I learnt the hard way that each additional sub-dependency can cost a company some thousands of dollars.

I forked and wrote lil-http-terminator in two hours. I foresee saving myself from 8 to 80 hours this way. You can save the same.

Discussion (0)