lil-http-terminator, a tiny JS module to gracefully shutdown your HTTP server
Tags: node, npm, javascript
Published: 2021-07-11 10:49:42

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`](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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:
```js
import { createHttpTerminator } from 'http-terminator';
const httpTerminator = createHttpTerminator({ server });
await httpTerminator.terminate();
```

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:

```shell
$ 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)
```

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:

```js
const delay = time => new Promise(r => setTimeout(r, time));
```

#### `roarr`

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

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

  return terminating;
}
```

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`](https://www.npmjs.com/package/lil-http-terminator).

```js
const HttpTerminator = require("lil-http-terminator");
const httpTerminator = HttpTerminator({ server });
await httpTerminator.terminate();
```

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`](https://www.npmjs.com/package/lil-http-terminator) in two hours. I foresee saving myself from 8 to 80 hours this way. You can save the same.