npm install: Because Why Write Code When You Can Download Problems?
Your âHello Worldâ React app somehow needs 50,000 dependencies. This isnât an exaggerationâitâs Tuesday in JavaScript land.
Modern JavaScript projects routinely ship with dependency trees that make the Amazon rainforest look sparse. A typical project has more third-party code than NASAâs Mars rover. Hereâs how to manage this without losing your sanity or your security clearance.
The Dependency Explosion: How We Got Here
JavaScript didnât ship with a standard library worth mentioning. While Python developers got batteries included and Go developers got a sensible core, JavaScript developers got⌠parseInt() and the promise that someone on npm would fill the gaps.
The community took that promise seriously. Too seriously.
$ ls -la my-simple-app/
total 1.2G
drwxr-xr-x 3 dev dev 96B Sep 3 10:30 .
drwxr-xr-x 4 dev dev 128B Sep 3 10:29 ..
-rw-r--r-- 1 dev dev 427B Sep 3 10:30 package.json
drwxr-xr-x 847 dev dev 1.2G Sep 3 10:30 node_modules
$ wc -l src/**/*.js
89 total
89 lines of application code. 1.2GB of dependencies. The tail is wagging the dog so hard it achieved orbital velocity.
The Left-Pad Incident: When 11 Lines Broke the Internet
March 22, 2016. A developer named Azer Koçulu got into a trademark dispute with npm and Kik over a package name. In frustration, he unpublished all his packages from npm, including a tiny utility called left-pad.
This 11-line function broke the internet:
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;
}
Babel couldnât build. React couldnât install. Facebookâs entire frontend toolchain ground to a halt. All because thousands of developers decided that writing eleven lines of code was too much work.
JavaScript now has String.prototype.padStart(), but the damage was done. The entire ecosystem learned it was built on a house of cards, and the response was⌠to keep adding more cards.
The Hidden Costs of Dependency Hell
Security: Youâre Only as Strong as Your Weakest Link
Every dependency is a potential attack vector. The event-stream package had 2 million weekly downloads when a new maintainer added Bitcoin-stealing malware. It went undetected for months.
But the real threat is more mundane: package ownership transfers. The original maintainer burns out, gets a new job, or simply moves on. They sell the package to someone else, or abandon it entirely. That new owner can push whatever they want to millions of production systems.
In 2021, security researchers compromised major companies by uploading malicious packages with names similar to internal ones (typosquatting attacks). Your security is only as good as your least-vetted transitive dependency.
The Transitive Dependency Explosion
When you install a package, youâre not just getting that package. Youâre getting every package it depends on, and every package those packages depend on, creating a deep tree of transitive dependencies.
$ npm install @mui/material
# You think you're getting one package
# You actually get 47 packages including:
# - clsx (for className utilities)
# - @babel/runtime (for polyfills)
# - prop-types (for React prop validation)
# - react-transition-group (for animations)
# - @emotion/react (CSS-in-JS engine)
# - And 42 others you've never heard of
Each of these has its own dependencies, creating a cascade that turns your innocent UI library into a small cityâs worth of JavaScript. Youâre now responsible for monitoring security vulnerabilities in packages youâve never heard of, written by people youâve never met, for use cases you donât understand.
The Breaking Change Lottery
Semantic versioning is supposed to make updates safe. In practice, semver is more like a suggestion that many package authors ignore.
When npm saves dependencies, it uses caret notation by default (^4.17.21). This tells npm to grab the latest compatible version, which sounds reasonable until you realize what âcompatibleâ means in practice.
# Monday: You install and get version 4.17.21
# Tuesday: Teammate installs and gets 4.17.22
# Wednesday: CI installs and gets 4.18.0
# Same package.json, three different dependency trees
Version bumps from 2.1.3 to 2.1.4 shouldnât break anything, but package maintainers regularly ship breaking changes in patch releases. They call them âbug fixes,â you call them âreasons to work late.â
Professional Dependency Management: Tools That Actually Work
The default tooling is holding you back. Hereâs what professionals use instead.
Lockfiles: Your Dependency Sanity Anchor
The solution to version chaos is the lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock). If package.json is your dependency wishlist, the lockfile is your dependency receipt.
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
This locks down the exact version, download URL, and cryptographic proof that this is the exact code you tested with.
Commit your lockfile. This isnât optional. Itâs the difference between âworks on my machineâ and âworks on every machine, forever.â
Beyond npm: Why pnpm is Worth the Switch
npm has a fundamental flaw: it âhoistsâ all dependencies to the top level of node_modules, creating a flat structure. This seems convenient until you realize it lets you import packages your project doesnât actually depend on.
// Your package.json only includes React
// But you can import this because React depends on it
import PropTypes from 'prop-types';
// This works locally but breaks in production
// because you never declared prop-types as a dependency
This is called a phantom dependency and itâs a ticking time bomb.
pnpm solves this elegantly:
-
Content-addressable storage: Instead of copying packages into every project, pnpm maintains a single global store and uses hard links. If you have Lodash in 10 projects, the files exist only once on disk.
-
Strict node_modules: No hoisting. You can only import whatâs in your
package.json. Phantom dependencies become build errors instead of production surprises. -
Speed: Hard links are faster than copying files. pnpm installations are consistently faster than npm.
Switching is trivial:
npm install -g pnpm
rm -rf node_modules package-lock.json
pnpm install
Automated Security: Stop Playing Security Roulette
Running npm audit manually is like checking your blood pressure only when you feel dizzy. GitHubâs Dependabot automates the tedious work:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
reviewers:
- "your-team/security"
Now when a vulnerability is discovered, Dependabot automatically creates a PR with the fix, vulnerability details, and changelog. Your security surface area shrinks automatically instead of relying on your memory to run audits.
The Discipline: Practical Hygiene Rules
Tools are only as good as the discipline around using them.
Project Configuration That Prevents Mistakes
Create .npmrc in your project root:
save-exact=true
engine-strict=true
fund=false
audit-level=moderate
save-exact=true: Removes caret prefixes, forces explicit version updatesengine-strict=true: Fails installs if Node.js version doesnât matchfund=false: Stops the spam messages begging for donationsaudit-level=moderate: Only shows actionable security warnings
The Pre-Installation Checklist
Before typing npm install, run through this checklist:
Can I write this myself in under 100 lines? If yes, write it. Youâll understand it completely, itâll have zero dependencies, and itâll never disappear from npm at 3 AM on a Friday.
Is this package actively maintained? Check GitHub. If the last commit was in 2019 and there are 47 open security issues, keep looking.
How many dependencies does this package have? Use npm info package-name dependencies. If a utility package has 20 dependencies, itâs not a utilityâitâs a liability.
Whatâs the bundle size impact? Use bundlephobia.com to see what youâre actually shipping to users. That innocent-looking date utility might be 300KB minified.
Embrace the Standard Library
Modern JavaScript has most utilities built-in. You donât need packages for:
// Array deduplication
const unique = [...new Set(array)];
// String padding
const padded = str.padStart(10, '0');
// Date formatting
const formatted = new Intl.DateTimeFormat('en-US').format(date);
// Deep cloning (with caveats)
const cloned = structuredClone(obj);
// Debouncing
const debounce = (fn, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
};
// Random ID generation
const id = crypto.randomUUID();
Bundle Analysis: Know What Youâre Shipping
For client-side code, use webpack-bundle-analyzer to visualize what youâre actually sending to users:
npx webpack-bundle-analyzer build/static/js/*.js
This creates an interactive treemap showing which packages contribute most to your bundle size. Youâll often find surprises like Lodash being imported in its entirety when you only use one function, or multiple versions of the same library.
When Dependencies Actually Make Sense
Some packages are worth the complexity:
Complex algorithms or protocols: Cryptography, image processing, HTTP/2, WebSocket handling. Donât roll your own crypto.
Cross-browser compatibility: Polyfills for older browsers, CSS vendor prefix handling.
Framework ecosystems: React, Vue, Angular and their official tooling.
Build and development tools: Webpack, Babel, TypeScript, testing frameworks.
Well-maintained utilities with single responsibilities: date-fns (if you need heavy date manipulation), validator.js (for input validation).
What to Avoid Like the Plague
Donât install packages for:
Simple utilities you can write in minutes: String manipulation, basic array operations, simple mathematical functions.
Micro-packages: is-number, is-array, is-even. Yes, is-even exists and has millions of downloads. Itâs literally n % 2 === 0.
Unmaintained code: Last update in 2018, dozens of open security issues, maintainer has vanished.
Kitchen sink libraries: Lodash is 70KB minified. You probably need three functions from it.
Framework alternatives for simple tasks: Donât install axios when fetch() works fine for your use case.
The Dependency Budget
Treat dependencies like a budget. Every addition has a cost:
- Build time: More packages = slower installs and builds
- Bundle size: Users download your dependencies too
- Security surface: More code = more potential vulnerabilities
- Maintenance overhead: Each package needs monitoring and updates
The goal isnât dependency-free developmentâthatâs as extreme as installing packages for everything. Itâs about being intentional.
Regular Health Checks
# Security audit (focus on production deps)
npm audit --production
# See what's outdated
npm outdated
# Find unused dependencies
npx depcheck
# Production deployment (exact versions)
npm ci
Set up automated dependency updates with Dependabot, but configure them to create separate PRs for each package. Mass updates are how you accidentally break everything.
The Reality Check
Every package you add is a bet. Youâre betting that:
- The maintainer wonât abandon it
- Security vulnerabilities will be patched quickly
- Breaking changes wonât ship in patch releases
- The package will still exist in five years
- The benefits outweigh the complexity
Sometimes that bet pays off. React has been a good bet for most people. Sometimes it doesnâtâjust ask anyone who built on Angular 1.x.
The Path Forward
The JavaScript ecosystem is slowly maturing. Web APIs handle more use cases every year. The language itself keeps adding utilities. Tools like Deno are pushing back against the âpackage for everythingâ mentality.
But weâre still living with the consequences of the early days. Your job as a developer is to be the adult in the room. Ask hard questions about every dependency. Push back on reflexive npm install habits.
Remember: youâre building software, not curating a package museum. Sometimes the best dependency is no dependency at all.
The difference between a maintainable project and a nightmare is discipline: using better tools (pnpm), automating security (Dependabot), enforcing good practices (proper configuration), and treating every npm install as a conscious architectural decision rather than a reflexive action.
Your future self will thank you when your builds are fast, your bundles are lean, and your security audit doesnât read like a CVE encyclopedia.