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();
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)
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));
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;
}
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();
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.
Top comments (1)
how would you shut down a websocket connection that uses WS here?