Chapter 17: npm, Dependency Hell, and the Supply Chain Problem
Open a package.json in any modern frontend project. Count the dependencies. Then run npm ls --all and count what’s actually installed.
The ratio is usually somewhere between 10x and 50x. A typical React project lists maybe fifty direct dependencies in its package.json and resolves them into a node_modules directory containing between 1,000 and 2,500 packages, maintained by between 800 and 1,800 distinct individual humans, distributed across the internet, downloaded over public CDNs, executing as postinstall scripts on developers’ machines, and forming a substantial fraction of every shipped JavaScript bundle.
This chapter is about that ratio — where it came from, what it cost, and why “the platform has no node_modules” is one of the strongest arguments the rest of this book is going to make.
npm and the Small-Modules Philosophy
Section titled “npm and the Small-Modules Philosophy”In 2010, Isaac Schlueter created npm.
Schlueter had been working with Node.js since shortly after Ryan Dahl introduced it, and the runtime — described in the previous chapter — needed a package manager. Node’s small-modules philosophy had emerged from the Unix tradition: programs should do one thing well, compose through clean interfaces, and stay small enough to be replaceable. The early Node community was deliberate about this. A package that did string manipulation didn’t also include date formatting. A package that parsed URLs didn’t also include HTTP requests. The granularity was small on purpose.
npm reflected and reinforced this culture. Publishing a package was easy. Depending on a package was easy. The registry grew quickly, and the number of packages each non-trivial application depended on grew with it. By 2015, popular utilities like Express depended on dozens of smaller packages, each of which depended on more. The dependency tree was the architecture.
In the absence of a richer standard library, the small-modules philosophy made sense. JavaScript didn’t have a padStart method until ES2017. It didn’t have native testing utilities. It didn’t have a built-in HTTP client until fetch arrived in Node 18. The ecosystem filled the standard library’s role, and the ecosystem was built out of thousands of tiny packages by independent maintainers.
The cost of that philosophy started showing up in 2016.
left-pad
Section titled “left-pad”On March 22, 2016, Azer Koçulu unpublished 273 of his npm packages.
Koçulu was a Turkish JavaScript developer who maintained, among many other things, a small package called kik. A messaging company also called Kik had asked Koçulu to rename the package; he had refused; the company had reportedly approached npm directly to claim the namespace. npm sided with the company. Koçulu, in protest, unpublished every package he maintained — kik and 272 others.
One of the unpublished packages was left-pad. The entire source of left-pad was eleven lines of JavaScript:
module.exports = leftpad
function leftpad (str, len, ch) { str = String(str) var i = -1 if (!ch && ch !== 0) ch = ' ' len = len - str.length while (++i < len) { str = ch + str } return str}The function takes a string and pads it on the left to a specified length. It’s eleven lines because JavaScript, in 2016, didn’t have a built-in String.prototype.padStart (added the following year in ES2017). Until ES2017 shipped, left-pad was the canonical implementation.
The package was depended on, directly or transitively, by an enormous portion of the npm ecosystem. Babel depended on it. React’s build tooling depended on it. Within hours of Koçulu’s unpublish, builds were breaking across the JavaScript world. The community panicked. npm took the unusual step of re-publishing left-pad under their own control, effectively overriding Koçulu’s right to remove his own work. The dependency trees came back to life. Builds started working again.
The technical story is small. The cultural moment was enormous. left-pad became the cleanest illustration of the problem: a single individual, in a single moment of frustration, could break the public web’s build pipeline because the world’s most popular JavaScript libraries depended transitively on his eleven lines of code.
The incident didn’t change much, structurally. npm tightened its unpublish policies. Some companies started vendoring their dependencies (committing node_modules directly to their own repositories) to avoid future disruptions. Most teams kept depending on thousands of packages from thousands of strangers, because the alternative was rebuilding too much.
event-stream
Section titled “event-stream”In November 2018, the maintainer of a popular npm package called event-stream admitted that he had transferred maintainer access to a new collaborator he’d never met. The new collaborator had then injected a payload into the package that targeted the bitcoin wallets of users running a specific cryptocurrency application.
The original maintainer was Dominic Tarr, a New Zealand-based JavaScript developer who had created event-stream years earlier and had stopped actively using it. The new collaborator approached him on GitHub, said they wanted to help maintain the package, and Tarr — with the kind of trust that had characterized npm’s culture for a decade — handed over publish access. The new collaborator made several legitimate contributions, then quietly added a malicious dependency, then used the malicious dependency to extract private keys from wallets stored on machines where the affected cryptocurrency app was running.
event-stream had two million downloads per week at the time of the incident. The malicious code was in the wild for almost three months before someone identified it.
Tarr’s public statement after the incident is one of the more honest reflections in the history of open source. He hadn’t done anything wrong, exactly — he had simply handed off a package to someone who turned out to be malicious. He hadn’t been paid for the maintenance. He hadn’t asked to be the steward of a piece of critical infrastructure. He had built a useful thing in his spare time, and the ecosystem had built itself on top of his useful thing, and the ecosystem had asked him to keep maintaining it forever.
The structural problem event-stream revealed wasn’t that someone wrote malicious code. The structural problem was that any of the thousands of npm packages maintained by unpaid individuals could be similarly compromised, and the ecosystem had no real defense against it.
ua-parser-js
Section titled “ua-parser-js”In October 2021, the maintainer of ua-parser-js — a widely-used library for parsing browser User-Agent strings — found that someone had compromised his npm account and published three versions of his package with malware embedded. The malware mined cryptocurrency on Linux and Windows machines and stole credentials.
ua-parser-js had roughly seven million weekly downloads at the time. The malicious versions were in the wild for several hours before the maintainer noticed and got them removed. CISA — the U.S. government’s cybersecurity agency — issued a public advisory. Companies whose CI pipelines had pulled the malicious versions had to audit their build systems for compromise.
The attack vector was a maintainer account takeover, likely through a phishing attack or a credential leak. The two-factor authentication that would have prevented it wasn’t yet required for npm package maintainers in 2021; it became required for the most-popular packages in 2022, and gradually expanded to more of the ecosystem in the years since.
The same month, similar account-takeover attacks hit coa and rc, two other widely-deployed npm packages. The pattern was consistent — compromise the maintainer’s account, push a malicious version, hope the bad versions propagate before they’re caught.
The Drumbeat Continues
Section titled “The Drumbeat Continues”The incidents above are the most famous. They aren’t the only ones, or even the most recent ones. A short list of major npm supply-chain incidents from 2022 through 2025:
The colors and faker self-sabotage incident, January 2022 — the maintainer of both packages intentionally pushed broken versions of his own work as a protest against unpaid maintenance burden.
The polyfill.io domain takeover, June 2024 — the polyfill.io CDN domain was sold to a Chinese company that began injecting malicious scripts into the JavaScript served from the domain. Sites that had embedded polyfill.io as a script tag (a common pattern dating from the early 2010s, when polyfills for missing browser features were essential) were unknowingly serving the malicious code to their users. Cloudflare and Google ultimately blocked the domain. An estimated 100,000+ websites had been affected.
The xz-utils backdoor, March 2024 — not npm, but the same shape of attack on a different ecosystem. A patient social-engineering campaign against a single overworked open-source maintainer resulted in a backdoor being added to a widely-deployed Linux compression utility. The backdoor was discovered by accident, before it reached most stable Linux distributions. The maintainer who had been pressured into accepting the malicious collaborator’s “help” was Lasse Collin, working unpaid on infrastructure used by millions of servers.
The pattern repeats often enough that the security community has stopped treating each incident as a surprise. It’s now a steady background of small compromises, with the occasional large one, against an ecosystem that has never had the institutional defenses to prevent them.
Log4Shell, as the Parallel
Section titled “Log4Shell, as the Parallel”In December 2021, a vulnerability disclosed in Apache Log4j — a Java logging library — caused a global incident that consumed weeks of every security team’s attention. The vulnerability, dubbed Log4Shell (CVE-2021-44228), let attackers execute arbitrary code on any server running an affected version of Log4j, which turned out to be a substantial fraction of all enterprise Java software in the world. Log4j was maintained by a small number of unpaid volunteers at the Apache Software Foundation.
Log4Shell wasn’t an npm incident. It was the same incident, in a different ecosystem, with the same structural cause. A library that almost nobody paid attention to had become quietly load-bearing for the world’s enterprise software. When a vulnerability surfaced, the world’s enterprise software had to be patched simultaneously, by a labor force that hadn’t been paid to maintain the underlying library and had no contractual obligation to fix it quickly.
The Log4j maintainers — Ralph Goers, Gary Gregory, and a handful of others — fixed the vulnerability under enormous pressure and without compensation. The incident became a wake-up call for governments and large companies about software supply chain risk. The U.S. White House held a summit on open source security in 2022. The EU passed the Cyber Resilience Act in 2024. The structural conditions that produced Log4Shell are still mostly in place.
The xkcd Comic
Section titled “The xkcd Comic”In August 2020, Randall Munroe published a comic on xkcd.com titled Dependency. The illustration shows a tall, precarious tower of blocks labeled all modern digital infrastructure. Most of the blocks are stable. One small block near the bottom, holding up the entire tower, is labeled a project some random person in Nebraska has been thanklessly maintaining since 2003.
The comic has been quoted in every subsequent discussion of software supply chain risk because it’s accurate. The frontend dependency tree isn’t different from the rest of digital infrastructure in this respect — it just has more pieces. The transitive dependencies of an average React application include hundreds of small packages, each maintained (often unpaid) by an individual with no contractual obligation to anyone using their work. The ecosystem functions because most of those individuals are decent and competent. The risk surface is what happens when one of them is compromised, burned out, or maliciously replaced.
The Structural Problem
Section titled “The Structural Problem”The structural problem is this. The frontend’s dependency model is transitive, centralized, and low-trust.
It’s transitive because depending on a package means depending on every package that package depends on. A typical npm install resolves a flat dependency list (the project’s package.json) into a tree of thousands of packages from hundreds of independent authors. The project’s developers usually have no idea what most of those packages are, who wrote them, or whether they’ve changed in the last month. The trust boundary is the registry, not the individual maintainer.
It’s centralized because the registry — npm Inc., now owned by GitHub, now owned by Microsoft — is a single piece of infrastructure that the entire ecosystem depends on. An outage at the npm registry stops new builds globally. A policy decision by npm Inc. (the left-pad re-publish, for example) overrides individual maintainers’ rights to their own work. Alternative registries exist (the npm protocol is open) but the network effects strongly favor the original.
It’s low-trust because the registry doesn’t meaningfully vet what gets published. Anyone can create an account and publish a package. Anyone with maintainer access on a package can push a new version. The default assumption is that whatever’s on npm is fine, and the burden of verifying that any given package is safe falls entirely on the consumer.
These three properties together produce the supply-chain incidents the chapter has been describing. The fix is structural, not incremental. The supply chain has to become less transitive, less centralized, or higher-trust — or all three — for the underlying risk to come down meaningfully.
What the Platform-First Answer Looks Like
Section titled “What the Platform-First Answer Looks Like”The book’s broader argument is that one piece of the answer is fewer dependencies, written against the platform, by your own team.
This is what “the platform has no node_modules” means in practice. The browser’s built-in capabilities — fetch, URL, URLSearchParams, FormData, Crypto, History, custom elements, observers, the storage tier, modules, async iteration, the full set of ECMAScript features — are stable, backward-compatible, and maintained by the standards bodies described in Chapters 5 and 6. Code written against the platform doesn’t pull a dependency tree. It doesn’t break when a maintainer unpublishes their work. It doesn’t get compromised by an account takeover. It is, in supply-chain terms, trivially auditable: the only third-party code in the application is the code the team has explicitly chosen to include.
The trade-off is real. Writing code against the platform takes more lines than wiring together npm packages. A repository pattern for talking to an API might be 100 lines of plain TypeScript instead of a one-line npm install axios. A small reactive store might be 50 lines instead of npm install zustand. A modal might be a <dialog> element with thirty lines of CSS instead of a 40KB modal library. The cost in lines is real. The cost in supply-chain risk, bundle size, and long-term maintainability is much lower.
This isn’t an argument that nobody should depend on anything. Plenty of well-maintained libraries still earn their place — Lit, the major build tools, the core frameworks themselves. The argument is about which abstractions are worth depending on, and the answer depends, in part, on what those abstractions are protecting you from. A library that wraps a platform capability you could’ve used directly is paying a supply-chain tax for very little upside. A library that provides a capability the platform doesn’t have, maintained by a team with institutional staying power, may be worth it.
The decoration-vs-replacement framing from earlier chapters applies here too. A decorating library leans on the platform and adds an opinion; if the library disappears, your code mostly still works against the platform underneath. A replacing library substitutes its own reality; if it disappears, your code disappears with it. The supply-chain math is much friendlier to decorating libraries.
What Comes Next
Section titled “What Comes Next”The next chapter is, in a way, about what happens when the entire dependency story stops being a frontend concern and becomes a desktop-software concern. Most of the developer tools you use today — VSCode, Slack, Discord, Notion, Figma, the desktop ChatGPT and Claude apps — are built on Electron, which is to say they’re built on Chromium and Node. The frontend’s dependency story has, quietly, become the substrate of most modern desktop software, and the implications of that fact for the rest of this book’s argument are worth taking seriously.
Exercise: Audit Your Dependencies
Section titled “Exercise: Audit Your Dependencies”Pick a frontend project you work on. Run npm ls --all (or the equivalent for your package manager).
Then answer:
- How many distinct packages are installed? How many are direct dependencies in your
package.json, and how many are transitive? - Pick five random packages from the installed list. For each one: who wrote it? When was its last release? How many other packages depend on it?
- For your project’s three largest direct dependencies (by transitive count), are there platform APIs that could replace some or all of what each does?
fetchfor an HTTP client?URLandURLSearchParamsfor URL handling?<dialog>for a modal library? - If the package registry went down for a week, what would your team’s workflow look like? Could builds happen from cached
node_modules? Could new dependencies be added? - If one of your transitive dependencies had an
event-stream-style compromise tomorrow, how would you find out? What’s the gap between the compromised version published and your team noticing?
The point isn’t to terrify yourself out of using npm. The point is to develop a working sense of the dependency surface your team is actually exposed to — and to start asking, for each new dependency you add, whether the value it provides is worth the long-tail risk it adds to the supply chain.