E01 — The Sorhus Pattern and the Modular Imperative
The micro-module is the Node.js ecosystem's default architecture. It is the correct default for leaf utilities and the wrong default for foundation frameworks. The awesome-nodejs list proves it.
Key Takeaways
- The micro-module pattern visible across ~50% of
awesome-nodejsentries is a deliberate design philosophy, not an accident of npm's permissive publishing model. - A Sindre-style micro-module (one verb, zero deps, single author) is the optimal unit of code at the *leaf* of the dependency graph — it composes well, fails small, and is easy to replace.
- The same pattern is anti-optimal at the *foundation* of the dependency graph (HTTP clients, web frameworks, ORMs), where a single point of failure becomes a single point of architectural stagnation.
- The list's most surprising property is not its breadth — it is the *authorship density* on leaf categories, which makes the ecosystem both resilient and brittle in the same gesture.
---
pify is forty lines. p-map is forty lines. onetime is twenty lines. camelcase is fifteen lines. indent-string is ten lines. Open any of them on GitHub and you will see the same skeleton: a single index.js, a readme.md, a license, a package.json with dependencies: {}, a maintainer field, and a test.js that is longer than the implementation. These are the entries that crowd the awesome-nodejs list under headings like Command-line utilities, Filesystem, Text, Number, Math, and Date. They are the verbs of the runtime. They are the most-cloned, least-discussed units of code on npm.
Now open express. Open fastify. Open mongoose. Open sequelize. Open axios. These entries are the nouns. They have *fan-in*: dozens, sometimes hundreds, of the leaf modules depend on them, transitively or directly. They are the foundations. They are the most-discussed, least-replaced units of code on npm.
The awesome-nodejs list contains both. The interesting question — and the one I want to spend this chapter on — is whether *both* should be designed the same way. My answer is no. The list, read carefully, shows that the Node.js community has, in practice, settled on two different architectures for two different roles, and that the boundary between them is one of the most consequential and least discussed decisions in the entire ecosystem.
The pattern, named
I will call the first architecture leaf micro-module: a package that does one thing, has zero or near-zero dependencies, fits in a single file, is owned by one person (often Sindre Sorhus), and is downloaded tens of millions of times per week. The standard-bearers in the list are chalk, got (the Sindre-era lineage), meow, ora, conf, chokidar, p-map, nanoid, dotenv, pify, delay, mem, camelcase, escape-string-regexp, string-width, normalize-url, pretty-bytes, pretty-ms, globby, del, move-file, tempy, camelcase, splice-string, indent-string, strip-indent, detect-indent, matcher, random-int, round-to, math-clamp, humanize-url, get-port, ipify, image-type, image-dimensions, find-up, load-json-file, write-json-file, filenamify, package-directory, onetime, strip-bom, os-locale, dot-prop, hasha, clipboardy, execa, open, get-stdin, log-update, log-symbols, figures, boxen, cli-cursor, cli-columns, terminal-size, terminal-link, terminal-image, cli-truncate, string-length, image-type, string-width, yn, sparkly, gradient-string, first-chunk-stream, pad-stream, into-stream, delay, p-map, pify, valvelet, promise-memoize, observable-to-promise, binary-extract, unhomoglyph, he, leven, franc, StegCloak, cat-names, dog-names, superheroes, supervillains, cows, superb, cool-ascii-faces, cat-ascii-faces, nerds, cfonts, ascii-charts, progress, cli-table3, drawille, omelette, shelljs, cross-env, insight, cost-of-modules, auto-install, discharge, themer, carbon-now-cli, cash-cli, taskbook, localtunnel, gh-home, npm-home, is-up, is-online, public-ip, trash, speed-test, pageres, cpy, fkill, clipboard-cli, dark-mode, wallpaper, is-online, empty-trash, squoosh, npkill, jscpd, xcode, xo, atmo, np, npm-name, tmpin, pjs, normit, David, license-checker, bcat, browser-run, Jsome, JSDoc, Jsome, mobicon, mobisplash, diff2html-cli, trymodule, cash-cli, cashify, parcel, pico, cli-box, omelette…
The list keeps going. You can see the shape: a long tail of single-verb packages, each one a sentence long, each one essentially a function call. The maintainer field is overwhelmingly one name.
I will call the second architecture foundation framework: a package that orchestrates many concerns, has deep dependency trees, is owned by an organization or a stable maintainer team, and is the substrate that other code is written *against*. The standard-bearers are express, fastify, next.js, nuxt, hapi, koa, nest, meteor, restify, loopback, actionhero, seneca, moleculer, adonisjs, hono, marble.js, tinyhttp, feathers, micro, lad, typegraphql, tsed, mongoose, sequelize, prisma, typeorm, drizzle-orm, mikro-orm, knex, bookshelf, pg-promise, ioredis, node-postgres, kafkajs, amqplib, mqtt.js, socket.io, µWebSockets, faye, primus, deepstream.io, socketcluster, bull, bullmq, agenda, bree, graphile-worker, passport, casbin, casl, nodemailer, pino, winston, eslint, prettier, webpack, rollup, parcel, vite, pkg, gulp, broccoli, brunch, fusebox, pm2, nodemon, nvm, fnm, n, pnpm, yarn, bun, npm, jest, vitest, mocha, ava, tap, puppeteer, playwright, webdriverio, nightwatch, testcafe, codeceptjs, sinon, nock, nyc, loadtest, testcontainers-node, nock, nve, axe-core, pino, winston, consola, console-log-level, debug, 0x, ctrace, llnode, swagger-stats, thetool, locus, why-is-node-running, leakage, njsTrace, vstream, stackman, nim, dats, rate-limiter-flexible, themis, upash, jose-simple, crypto-hash, marked, remark, markdown-it, parse5, cheerio, jsdom, puppeteer, playwright, axe-core, pdfkit, xlsx, sheetjs, isomorphic-git, js-git, nodegit, webtorrent, ipfs, helia, nodeos, yodaos, brain.js, pipcook, cytoscape.js, bitcoinjs, bitcore, turf, webcat, peerflix, peercast, peerwiki, stackgl, pdfkit…
Again, the list keeps going. The shape here is different: fewer entries per category, larger maintainer teams, more conservative release cadences, deeper dependency trees. These are the nouns.
Why the leaf is right
I want to start with the leaf, because the leaf is where the case is easiest to make.
Imagine you're writing a CLI tool. You need to print a colored message, parse arguments, show a spinner, and read JSON from stdin. You reach for chalk, meow, ora, get-stdin. Four packages. Each is one file. Each has zero dependencies. Each can be replaced in five minutes with a hand-rolled alternative if the maintainer goes away. The composition cost is the cost of npm install and four import statements. The blast radius of any one of them failing is bounded — chalk going down does not break your spinner. ora going down does not break your argument parser. Each is a single verb, in a single file, written by a single person, with a single test file, versioned in a single repo, and downloadable in milliseconds.
This is, in my judgment, the *correct* unit of code for a leaf concern. The reasons are not aesthetic — they are mechanical. A small package:
- Fails small. When
indent-stringhas a bug, your indentation is wrong. The blast radius is a single concern. - Composes well. A small package has a single input and a single output. Composition is a function call. There is no surface to integrate.
- Replaces easily. When
indent-stringis unmaintained, you reimplement it in twelve lines. Your upgrade path isgit rmand a one-liner. - Versions cleanly. Semantic versioning is meaningful at this granularity. A patch release of
indent-stringcannot, by construction, change the meaning ofchalk. - Audits cheaply.
npm auditon a single-file package takes milliseconds. The supply chain attack surface is one file.
I came into this analysis believing the conventional wisdom: that "too many small packages" is a problem, that node_modules is bloated because of people like Sindre. I now believe the opposite. The leaf micro-module is the highest-quality unit of code the JavaScript ecosystem has produced, and its dominance is the ecosystem's most defensible architectural achievement. The reason node_modules is bloated is not because of pify. It is because of express pulling in 47 transitive dependencies, and mongoose pulling in 28, and webpack pulling in 600. The leaf is not the problem. The leaf is the solution the ecosystem found to the problem of reusable verbs in a language without a standard library.
The list confirms this empirically. The categories with the most entries — Command-line utilities (~50), Testing (~30), Database (~40), Filesystem (~20), Text (~20) — are the categories where the micro-module pattern is dominant. The categories with the fewest entries — Forum (1), Blogging (2), AST (2), Hardware (~10) — are the categories where the foundation pattern is dominant. The pattern follows function: concerns that are decomposable into single verbs are dense with micro-modules. Concerns that are not, are not.
Why the foundation is wrong
Now the harder argument. The same architectural pattern — small, single-purpose, single-author — is *anti-optimal* for foundation frameworks, and the list shows that the Node.js community has, in places, applied the leaf pattern to foundation concerns and produced a category of package that is, structurally, in permanent crisis.
Imagine you're building a web service. You reach for express. express is a foundation framework. It is also, depending on the release, a 4,000- to 12,000-line monolith with ~30 transitive dependencies, owned by a small core team, with a release cadence measured in years. It is the most-installed package in the Node ecosystem. It is also, by every public benchmark, slower than fastify, slower than hono, slower than koa, and slower than tinyhttp. It has not been *rewritten* since 2014. It has been *patched* since 2014.
This is the leaf pattern misapplied. The leaf pattern assumes you can replace a package in five minutes. You cannot replace express in five minutes. Your entire routing layer, your middleware chain, your error-handling contract, your third-party plugin ecosystem, and your team's mental model are all coupled to express's specific shape. Replacement is a multi-month migration. The leaf pattern's central promise — *fails small, replaces easily* — does not survive the trip from leaf to foundation.
The same critique applies, with varying severity, to mongoose, sequelize, socket.io, webpack, mocha, eslint, request (the historical HTTP client, now deprecated), grunt, gulp (in its post-stream era), and npm itself. Each is a foundation framework that has been patched rather than rewritten, that has accumulated decades of edge cases, that has a maintainer team of one-to-five, and that the ecosystem has, in aggregate, become *structurally dependent* on.
The list shows this asymmetry clearly. The foundation categories (Web frameworks, Database, Build tools, Node.js management, Process management) have *fewer* entries than the leaf categories, but each entry is heavier, older, and more entrenched. The leaf categories are vibrant, competitive, and replaceable. The foundation categories are concentrated, ossified, and load-bearing.
The maintainer concentration problem
Here is where the analysis gets uncomfortable, and where I want to be very precise.
The leaf micro-module works because *any one of them* can fail without breaking the ecosystem. The risk is distributed across thousands of single-verb packages. The maintenance burden of any one leaf is small. The replacement cost is small. The blast radius is small.
The foundation framework does not have this property. When express slows down, the entire ecosystem slows down. When mongoose ships a security vulnerability, the entire ecosystem ships the vulnerability. When webpack is unmaintained for a year, every frontend in the world waits.
And the foundation pattern, as currently practiced in the Node ecosystem, has a *single-maintainer risk* that the leaf pattern does not.
Look at the list. got — historically Sindre, now archived in favor of the sindresorhus/got lineage, but the replacement still has a small core team. chalk — Sindre, now a multi-maintainer project after the 2022 controversy. pino — single primary maintainer. mongoose — Automattic-backed, but a small core team. fastify — multi-org, but a small core team. webpack — single primary maintainer for years. mocha — single primary maintainer for years. eslint — single primary maintainer for years. jest — Meta, but a small core team. next.js — Vercel, but a small core team. vue / nuxt — Evan You, plus a small core team. sveltekit — Rich Harris plus a small core team.
The pattern is not Sindre. The pattern is *the whole ecosystem*. Foundation frameworks in Node.js are, on average, owned by one-to-five humans, with one-to-two of them carrying the load. This is not a critique. It is a structural observation. The reason is that foundation frameworks are hard to maintain: they have to maintain backward compatibility, they have to manage a long tail of edge cases, they have to negotiate with a large user base, and they have to do this for years without compensation that competes with industry salaries. The micro-module pattern survives because the maintenance burden of a leaf is small. The foundation pattern struggles because the maintenance burden of a foundation is large, and the economic structure of open source does not reward it.
graph LR
subgraph Leaf["Leaf micro-module (correct default)"]
A1["pify<br/>40 LOC"]
A2["camelcase<br/>15 LOC"]
A3["indent-string<br/>10 LOC"]
A4["onetime<br/>20 LOC"]
A5["mem<br/>30 LOC"]
end
subgraph Foundation["Foundation framework (anti-pattern at this scale)"]
B1["express<br/>~10,000 LOC"]
B2["mongoose<br/>~25,000 LOC"]
B3["webpack<br/>~80,000 LOC"]
B4["mocha<br/>~15,000 LOC"]
end
subgraph Risk["Maintainer concentration"]
C1["Sindre-shaped hole<br/>(~50 leaf entries)"]
C2["Single-maintainer<br/>foundation"]
end
Leaf --> Risk
Foundation --> Risk
style Leaf fill:#1f3a5f,stroke:#88a,color:#fff
style Foundation fill:#5f1f1f,stroke:#a55,color:#fff
style Risk fill:#3a3a1f,stroke:#aa8,color:#fff
The diagram is the architectural fingerprint of the list. The leaf cluster is dense, parallel, and replaceable. The foundation cluster is sparse, sequential, and load-bearing. The risk surface — a Sindre-shaped hole at the leaf, a single-maintainer foundation at the base — is concentrated at the *boundaries* between the two layers.
The decision framework
I want to leave you with a decision framework, derived from the list, that you can use the next time you reach for npm install.
| Concern | Pattern | Example | Why | |---|---|---|---| | Single-verb utility (format, parse, transform) | Leaf micro-module | chalk, pify, camelcase | Fails small, replaces easily | | Multi-verb infrastructure (file watching, network) | Small focused package | chokidar, got, nanoid | Bounded scope, owned by one team | | Cross-cutting infrastructure (logging, config) | Either, with vetting | pino, dotenv | Depends on lockfile discipline | | HTTP framework / ORM / build tool | Foundation, vetted | express, fastify, prisma | Avoid single-maintainer critical paths | | Anything security-sensitive | Foundation with a corporate backer | passport, node-postgres | Single-author security is a hazard |
The framework is not original. It is the implicit framework the list has been following for a decade. The list is, in effect, the artifact of ten years of curation decisions made by a community that has been voting with its npm install on which pattern to apply to which concern.
What this means for the runtime
The list predicts the next decade of Node.js the way a fossil predicts the next geological era. The leaf layer will continue to grow. It will continue to be owned, in aggregate, by a small number of prolific individuals, of whom Sindre is the exemplar. The risk at the leaf layer is *replacement cost*, not *maintainer concentration*, because the leaf pattern is, by construction, replaceable.
The foundation layer will continue to ossify. The risk at the foundation layer is *maintainer concentration*, and it is not replaceable. The most consequential question for the Node ecosystem in 2030 is not "what new micro-module will Sindre ship next." It is "who is going to maintain express when its current core team is no longer available, and what will the migration path be for the millions of services that depend on it."
I do not have a clean answer to that question. I do not think the list has one either. But the list has, in its 927 lines and ~900 entries, the most honest accounting of the structural risk that I have seen anywhere. The most important fact in the entire list is not the Sindre footprint. It is the *category asymmetry* — the leaf is replaceable and resilient, the foundation is load-bearing and fragile, and the boundary between them is where the next decade of architectural decisions will be made or fumbled.
That boundary is what I want you to take from this chapter. The next time you npm install a single-verb package, you are voting for a pattern that works. The next time you npm install a foundation framework without checking who maintains it and what their succession plan is, you are voting for a pattern that does not. The list gives you the data. The judgment is yours.
---
References:
- sindresorhus/awesome-nodejs —
Web frameworks— the foundation-layer density - sindresorhus/awesome-nodejs —
Command-line utilities— the leaf-layer density - Express.js GitHub repository — the canonical foundation framework, ~10,000 LOC
- Fastify benchmarks — the foundation layer's competitive pressure (or lack of it)
- The Node Way —
FredKSchott/the-node-way— the philosophy the leaf pattern implements - 10 Things I Regret About Node.js — Ryan Dahl, 2018 — the runtime creator's retrospective on the decisions that produced both the leaf and the foundation patterns
- npm: A decade of micro-modules —
goldbergyoni/nodebestpractices— the empirical case for the leaf pattern