npm install regret: A Field Guide to Open Source Dependency Sprawl
Photo: developer laptop code terminal dark screen package dependencies, via thumbs.dreamstime.com
It starts so innocently. You need to parse some dates. You could write the logic yourself, but there's a library with 40,000 GitHub stars that does exactly this. You install it. It works great. You move on.
Six months later, your security scanner flags a critical CVE in a package you've never heard of. You trace it back. It's a transitive dependency of a transitive dependency of the date parsing library. The fix requires upgrading the date library to a new major version that has breaking API changes. The upgrade takes three days. Your junior dev, who wasn't on the team when this library was added, spends most of those three days confused about why any of this is necessary.
Welcome to dependency debt. Population: almost every JavaScript project ever written.
The Illusion of Free Code
Open source is one of the genuine miracles of software development. The idea that you can pull in years of someone else's carefully crafted work, for free, and build on top of it — that's not something you should take for granted. It's extraordinary.
But "free" in the financial sense has never meant "free" in the maintenance sense, and this is where a lot of development teams, especially early-stage ones moving fast, get themselves into trouble.
Every dependency you add to a project is a relationship. You're agreeing to keep up with that library's changes, to manage its compatibility with your other dependencies, to monitor it for security vulnerabilities, and to deal with the consequences when it's abandoned, broken, or compromised. For a library that sits at the core of your architecture, that's a relationship worth entering deliberately. For a library you added because it had a clever API and seemed easier than writing 40 lines of code yourself, it might be a relationship you'll regret.
The Transitive Dependency Problem
Here's the part that catches even experienced developers off guard: when you install a package, you're not just installing that package. You're installing everything it depends on, and everything those packages depend on, all the way down.
Run [npm](https://en.wikipedia.org/wiki/Npm) ls on a mid-sized Node project sometime and count the entries. A project with 30 direct dependencies might have 400 or 500 packages actually installed. Most of those are things you've never heard of, written by people you don't know, maintained (or not maintained) at a cadence you have no visibility into.
Each of those packages is attack surface. Each one is a potential breaking change. Each one is a thing that needs to be audited when your security team asks "what's running in our production environment?"
The left-pad incident from 2016 is the canonical example here — an 11-line package that got unpublished and broke builds across a significant chunk of the JavaScript ecosystem. But the more common and more dangerous version of this story isn't the dramatic unpublishing. It's the slow accumulation of packages that don't get updated, that accumulate vulnerabilities, that become compatibility anchors preventing the rest of your stack from upgrading.
The Junior Dev Inheritance Problem
There's a specific kind of technical debt that's particularly unfair, and dependency sprawl is a prime example of it: the debt that gets inherited by whoever joins the team after it was created.
The engineer who added six utility libraries in 2022 because they were convenient understood the tradeoffs at the time (or at least could have, if they'd thought about it). The engineer who joins in 2024 and has to upgrade those libraries, or debug a conflict between them, or explain to a security auditor why they're there — that person is paying a cost they didn't incur.
This matters for team culture and retention, not just code quality. Engineers don't like maintaining systems they don't understand. Dependency tangles they didn't create, for reasons they can't reconstruct, are demoralizing in a way that's hard to articulate but very easy to feel.
Build vs. Borrow: A Decision Framework That Actually Works
The solution isn't to never use open source. That would be absurd. The solution is to be deliberate about when you reach for a dependency versus when you write the code yourself.
Here's a framework that's practical enough to actually use:
Ask if the problem is core or peripheral. If the thing you're solving is directly related to what your product does — the thing you're actually selling — you should probably own that code. If it's genuinely infrastructure-level (logging, date formatting, HTTP requests), borrowing is usually fine. The risk is in the middle: the "helper" libraries that feel peripheral but quietly become load-bearing.
Count the lines before you install. Before adding a dependency, spend five minutes estimating how many lines of code it would take to implement the specific functionality you actually need — not the whole library, just the piece you're using. If the answer is "under 50 lines," the maintenance burden of a dependency probably isn't worth it. Copy the logic, paste it into a utils file, and add a comment explaining where it came from.
Check the maintenance signals. When was the last commit? How quickly do maintainers respond to issues? Is there an active community, or is this a library that one person built for a conference talk in 2019 and hasn't touched since? A library that's unmaintained isn't free code — it's a future migration project.
Audit your transitive surface area. Before adding a package, run a quick check on its own dependencies. A library with 40 transitive dependencies is a very different proposition from one with two. Tools like npm audit and bundlephobia.com make this easier than it used to be.
Set a policy for major version upgrades. One of the most common ways dependency debt accumulates is that teams install a library, use it, and then never upgrade it because upgrades are disruptive and there's always something more urgent. Decide upfront how you'll handle this — a quarterly dependency review, automated PRs from Dependabot, whatever fits your workflow — and actually do it.
Auditing the Spell Book You Already Have
If you're reading this with a growing sense of recognition about your current project, the good news is that auditing existing dependencies is tractable. It's not fun, but it's tractable.
Start by listing every direct dependency and asking a simple question: if this library disappeared tomorrow, how hard would it be to replace? That question surfaces both your riskiest dependencies (hard to replace, poorly maintained) and your most unnecessary ones (easy to replace, maybe should have been replaced already).
For each dependency that raises a flag, you have three options: migrate off it (the right call for abandoned or vulnerable packages), pin and isolate it (wrap it in an internal module so you can swap it out later without touching call sites throughout your codebase), or accept the risk consciously (sometimes a dependency is risky and irreplaceable and you just need to monitor it carefully).
The worst outcome isn't choosing any of these three options. It's not knowing the dependency is there.
The Borrowing Mindset vs. The Ownership Mindset
Ultimately, the dependency problem is a mindset problem as much as a technical one. The default in a lot of fast-moving teams is a borrowing mindset — if someone else built it, use it, ship faster, move on. That mindset isn't wrong, but it needs to be paired with an ownership mindset about what ends up in production.
You own everything in your dependency tree. Not legally, but operationally. When it breaks, you fix it. When it has a CVE, you patch it. When it stops being maintained, you migrate off it.
Install accordingly.