Think Before You npm install
Adding a dependency feels free. You run npm install,
import a function, and move on. But every package you add is code
you did not write, do not control, and probably never read. For
complex problems, third-party libraries make sense. For simple
things, they introduce risk you do not need.
What can go wrong
In 2024, the xz compression library was backdoored by
a contributor who spent two years building trust with the maintainer.
The backdoor targeted SSH authentication on Linux systems. It was
found by accident. A Microsoft engineer noticed SSH was slightly
slower than expected and traced it to the compromised library.
This is not an isolated case. The event-stream incident
in 2018 followed the same pattern — a trusted maintainer handed the
project to someone who injected code targeting a cryptocurrency
wallet. The colors and faker packages were
sabotaged by their own author in 2022. Supply chain attacks are not
theoretical risks. They happen regularly.
Even without malicious intent, dependencies break. A maintainer pushes a bad release. A transitive dependency changes its API. A package gets deprecated with no migration path. You now have a bug in code you did not write and may not understand.
The hidden cost of "saving time"
A common argument for using a library is that it saves time. Sometimes it does. But for simple functionality, the math often works out differently than you expect.
Say you need to debounce a function. You could install
lodash for _.debounce. Or you could write
this:
function debounce(fn, ms) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}
That is six lines. You understand every one of them. There is no dependency to update, no breaking change to worry about, no supply chain to trust. If the behavior needs to change, you change it.
Now compare: lodash is a large package. Even if you use
a tree-shakeable import like lodash-es/debounce, you
are pulling in code that handles edge cases you will never encounter.
Your bundle gets bigger. Your node_modules gets deeper.
Your lock file gets longer.
More examples of things you can write yourself
Deep cloning an object? For most cases,
structuredClone(obj) is built into the platform now. No
library needed.
Generating a unique ID? crypto.randomUUID() exists in
browsers and Node.js. You do not need uuid.
Formatting a date for display? Intl.DateTimeFormat
handles most formatting needs. You probably do not need
date-fns for showing "March 15, 2026" on a page.
Checking if a value is empty? Write a function. It takes two minutes and does exactly what you mean by "empty" in your project.
function isEmpty(value) {
if (value == null) return true;
if (typeof value === 'string') return value.trim().length === 0;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === 'object') return Object.keys(value).length === 0;
return false;
}
This does not handle every edge case that a library would. It does not need to. It handles your cases, and you can see exactly what it does.
When libraries are worth it
Not every dependency is a mistake. Some problems are genuinely hard, and writing your own solution would be worse than using a tested library.
Cryptography. Do not write your own. Use established libraries that have been audited. The failure modes are invisible and catastrophic.
Date and time zone math. Displaying a formatted date
is simple. Calculating the difference between two dates across time
zones and daylight saving transitions is not. Libraries like
date-fns or Temporal (when it lands) exist
because this problem is full of subtle bugs.
Parsing complex formats. Writing a Markdown parser, a CSV parser that handles all the edge cases, or an HTML sanitizer is real work. These are solved problems with well-maintained libraries.
UI component systems. Building an accessible date picker, combobox, or modal from scratch takes weeks of work to get right across browsers and screen readers. Headless UI libraries like Radix or React Aria save real time here.
The rule is not "never use libraries." It is "use libraries when the problem is complex enough to justify the trade-off."
How to evaluate a dependency
Before adding a package, ask a few questions. How many lines of code would it take to do this yourself? If the answer is under fifty, write it yourself. Does the package have many maintainers, or is it one person? Single-maintainer projects are a risk — people burn out, lose interest, or disappear. When was the last release? A package that has not been updated in three years might be stable or might be abandoned. Check the issue tracker. How many open issues does it have, and how many dependencies does it pull in? A simple utility that brings in twenty transitive dependencies is not simple at all.
Run npm ls on your project some time. The tree is
almost certainly deeper than you think.
Maintenance is the real cost
The install is the beginning, not the end. Every dependency needs to be kept up to date. Security patches need to be applied. Major version upgrades need to be tested. When a package is deprecated, you need to find an alternative and migrate.
Code you wrote yourself does not have these problems. It does not
release breaking changes when you are not looking. It does not get
flagged by npm audit because some transitive dependency
four levels deep has a prototype pollution vulnerability. It does not
need a Renovate bot filing pull requests every week.
Your own code is also easier to debug. When something breaks in a library, you read through unfamiliar code, figure out the abstraction layers, and hope the source maps are accurate. When something breaks in your own utility function, you open one file and fix it.
A practical approach
Keep a utils/ directory in your project. When you reach
for a library to do something small, write a utility function
instead. Add a few tests. Move on. Over time, you build a collection
of small, well-understood functions that do exactly what your project
needs.
For the big problems — the ones where writing your own would take weeks and introduce bugs that experts have already solved — use a library. Pick one that is well-maintained, widely used, and has a small dependency footprint.
The goal is not zero dependencies. It is intentional dependencies.
Every package in your package.json should be there
because you made a deliberate choice, not because it was the first
search result on npm.