How to reduce dependencies in a project
How to reduce dependencies in a project, with practical examples, common mistakes, and the cautions to apply the cleanup safely.
You installed a package to avoid writing three lines
It was supposed to be quick: you needed to check whether a string was empty, ran npm install on some package, moved on. Six months later, an audit flags a high-severity vulnerability in a transitive dependency three levels below that tiny package that exists only to do str.trim().length === 0. You didn't choose that dependency. It came along for the ride. And now it's your problem, in your deploy, on your pager.
This piece is about reducing dependencies deliberately, with practical examples and the cautions to avoid overdoing the cleanup. The bigger risk isn't even the dependency you can see — it's the abandoned one, that nobody updates and that one day becomes an entry point; on that it's worth reading how to spot abandoned dependencies.
Every dependency is a liability, not an asset
We treat package.json like a shopping list: found it useful, add it. But every dependency is a liability you take on forever. It widens the attack surface (supply chain), weighs on the bundle, demands maintenance when a breaking change ships, and opens the door to a version conflict with another dep that wants the same lib at a different major.
The left-pad incident back in 2016 should have ended the conversation. An eleven-line package that padded a string with leading zeros was pulled from npm and broke builds worldwide, including huge projects. Eleven lines. The lesson isn't "never use dependencies," it's "know what you're outsourcing and why." Outsourcing isArray is handing your house key to someone you've never met.
When to internalize and when to pull a package
The question I ask before installing anything: is the cost of keeping this in my codebase higher or lower than the cost of maintaining the dependency? For small, stable libs, internalizing almost always wins.
- Internalize when it's little logic, no nasty edge cases, and you fully understand it. A debounce, a simple slugify, a basic currency formatter. Ten to thirty tested lines beat a live dependency.
- Pull the package when the domain is treacherous and full of traps: date and timezone parsing, cryptography, PDF parsing, internationalization. Here, writing it from scratch is arrogance. Use the mature lib, with a community and tests you'd never cover on your own.
The rule of thumb: if you can write and test it in an afternoon, and the domain has no edge-case landmines, internalize it.
Audit what you actually use
A good chunk of an old project's dependencies aren't used by anyone anymore. The code that imported them was removed, but package.json still carries the weight. Before cutting, find out where each thing comes from:
# why is this dependency here? who pulled it in?
npm ls cross-spawn
# full tree, production only
npm ls --omit=dev --all
# what's out of date
npm outdated
npm ls <package> shows the chain: often you discover you didn't install that directly, it's transitive from something else. That's the most dangerous blind spot — you audit your twenty direct dependencies and ignore the three hundred transitive ones that tagged along. The transitive tree is where the real supply-chain risk lives.
To inspect package.json and the lockfile calmly, formatting the JSON helps you see the structure without getting lost in nested braces; I use the JSON Formatter to make package-lock.json readable before tracing where a version came from.
Separate dev from prod for real
A silent but common mistake: a test runner, linter, bundler, or TypeScript types installed as a production dependency. That inflates the tree that ships to runtime and, in some build setups, ends up in the final artifact.
# this is build-time, not runtime
npm install --save-dev vitest eslint @types/node
# this runs in production
npm install zod
The right question: does this package need to exist while the app is running in production? If the answer is no — it's tooling, it's only build — then --save-dev, always. Keeping that boundary clean reduces the surface that actually matters and keeps npm ls --omit=dev honest.
The standard library first
Before installing anything, there's the boring question worth asking: doesn't the language already do this? Array.prototype.flat, Object.fromEntries, structuredClone, crypto.randomUUID, Intl.NumberFormat — a lot that justified a package five years ago is native today. In Python and Go the effect is even stronger: the stdlib covers HTTP, JSON, hashing, and dates with zero external dependencies.
Every time you solve something with the standard library, you eliminate a maintenance liability, a supply-chain vector, and a node in the version tree. The stdlib isn't abandoned, has no surprise CVE from a vanished maintainer, and versions alongside the runtime. It's the cheapest dependency there is, because you already have it.
Frequently asked questions
Does reducing dependencies actually improve security?
Yes, and directly. Fewer dependencies mean less third-party code running in your process, fewer transitives to audit, and less chance a compromised package slips in through the chain. You don't eliminate supply-chain risk, but you drastically shrink its surface.
How do I know if a dependency is still used in the project?
Combine the tree (npm ls) with a source search for the package name. If nothing imports it directly and it isn't transitive of something needed, it's a removal candidate. Import-analysis tools also help flag orphaned dependencies.
Isn't internalizing code reinventing the wheel?
For complex domains, yes, and there you shouldn't do it. But copying thirty tested lines of a trivial utility isn't reinventing the wheel, it's taking ownership of something you already fully understand. The balance is reserving dependencies for what's genuinely hard.
Is bundle size worth worrying about on the backend?
On the backend the bundle weighs less, but the transitive tree still matters for install time, attack surface, and cold-start time in serverless environments. Even without the browser download problem, fewer dependencies mean faster deploys and less to go wrong.
What to take to the next install
Before the next npm install, ask: is this a liability worth taking on? Prefer the standard library, internalize what's trivial and stable, and reserve dependencies for genuinely hard domains. Audit the transitive tree, not just your direct deps, and keep the dev/prod boundary clean. A dependency isn't free — it's a maintenance promise you make to the future. Make fewer promises.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.