Skip to main content
Tech Stack progress
intermediatePage 9 of 27

Package Managers

How dependencies get installed and managed. pnpm, Bun, npm, Yarn — and which to pick.

Package Managers

In one line: A package manager installs and updates the libraries your project depends on. Bun is fastest; pnpm is strictest; npm comes with Node and works fine.

In plain English

Your project depends on hundreds of libraries — React, Tailwind, TypeScript, etc. — written by other people and published to npm (the central registry). A package manager is the tool that downloads those libraries to your machine, keeps them current, and locks the exact versions so your teammates get the same setup. The choice mostly affects speed and disk usage, not correctness.

pnpm — fast and efficient

pnpm uses a content-addressable store: every package version is stored once on disk and hardlinked into projects. Saves enormous amounts of disk space; install is fast.

npm install -g pnpm
pnpm install
pnpm add react

Why teams choose it:

  • Faster than npm.
  • Disk-efficient (huge for monorepos).
  • Strict dependency resolution (catches phantom dependencies).
  • Excellent monorepo support.

Bun (as a package manager)

The fastest package installer available. Compatible with package.json.

bun install # Install everything in package.json
bun add react # Add a dependency
bun add -D vitest # Add a dev dependency
bun remove react # Remove a dependency

Bun's package installer is dramatically faster than npm/pnpm/yarn — often 10–30× faster on cold installs.

npm

Comes bundled with Node.js. Fine for simple projects. Slower than alternatives but universally available.

npm install
npm install react

Yarn

Was the popular alternative to npm in the late 2010s. Yarn 1 is legacy; Yarn 4 (modern, with PnP and workspaces) is innovative but niche.

Decision matrix

Use caseRecommendation
Solo projects / small teamsBun for speed
Larger teams / monorepospnpm for strictness
Just learning, on Node alreadynpm is fine
Yarn shop alreadyYarn 4 if modernized; otherwise switch
Highlight: the lockfile is the source of truth

Whichever package manager you choose, commit the lockfile (package-lock.json, pnpm-lock.yaml, bun.lock, yarn.lock). The lockfile records the exact version of every package — including transitive dependencies — that was installed. Without it, your teammates and CI servers can get different versions and you'll hit "works on my machine" bugs.

The lockfile is more important than which package manager produced it.

Try it yourself
# In an empty folder:
echo '{"name":"test","version":"1.0.0"}' > package.json

# Time each one (cold cache):
time npm install react
time pnpm install react
time bun add react

You'll see a dramatic spread. Bun is usually 5–10× faster than npm on the same machine, and pnpm sits comfortably in between.

Supply-chain security

When you install a package, you're not just running its code — you're running the code of everything it depends on, transitively, often hundreds of packages written by people you've never heard of. That's the software supply chain, and it's a real attack surface: a compromised dependency runs with the same access your project has. A few concept-first defenses, roughly in order of effort:

npm audit — know your known vulnerabilities. Every package manager can cross-check your installed versions against a public database of disclosed vulnerabilities.

npm audit # list known CVEs in your dependency tree
npm audit fix # upgrade to patched versions where it's safe

It only catches publicly disclosed issues, and audit fix won't apply upgrades that would break your version ranges — but it's the cheapest first look at "am I shipping a known hole?"

Dependabot / Renovate — keep dependencies fresh automatically. Staying patched by hand doesn't scale. Dependabot (built into GitHub) and Renovate (configurable, works anywhere) watch your lockfile and open pull requests when a dependency has a newer or security-patched version. Your CI runs the test suite against each PR, so you review a small, isolated bump instead of a giant once-a-year upgrade. The win is small, continuous, tested updates rather than a scary big-bang.

The lockfile is a defense, not just reproducibility. You already commit package-lock.json (or pnpm-lock.yaml, bun.lock) for reproducible installs — but it's also a security boundary. The lockfile pins the exact resolved version and an integrity hash of every package. That means a teammate's stray install can't silently pull a brand-new 1.4.0 of a dependency, and a tampered package whose contents don't match the recorded hash fails the install. Deleting the lockfile to "clean up" throws that protection away.

Typosquatting and postinstall scripts — the two classic traps.

  • Typosquatting — Attackers publish malicious packages with names one keystroke off a popular one (reactt, loadsh, crossenv vs cross-env). A typo in npm install and you've installed the attacker's code. Slow down on install commands, copy names from official docs, and watch for a package with suspiciously few downloads where you expected millions.
  • Postinstall scripts — A package can run arbitrary code on your machine at install time via lifecycle scripts (postinstall). That's how a malicious dependency steals environment variables or tokens before you ever import it. You can disable lifecycle scripts by default (npm install --ignore-scripts, or pnpm's allowlist for which packages may run scripts) and re-enable only the few that genuinely need them.
Highlight: defense in depth, cheap to expensive

No single control is enough. The practical stack: commit the lockfile (free, do it always) → run npm audit in CI (cheap) → turn on Dependabot/Renovate (set up once, runs forever) → restrict postinstall scripts and double-check package names (habit). Each layer catches what the others miss.

Common mistakes

Where people commonly trip up
  • Mixing package managers in one repo. Running npm install in a project with a pnpm-lock.yaml, or bun install next to a package-lock.json, produces a second lockfile and silently different versions. Pick one per repo, commit its lockfile, delete the others.
  • Adding node_modules to git or .gitignoring the lockfile. Exactly backwards. Lockfile in git; node_modules out. The lockfile is the contract; node_modules is regenerated from it.
  • Running npm install in CI instead of npm ci (or pnpm install --frozen-lockfile). Plain install is allowed to mutate the lockfile, which means CI can quietly upgrade transitive deps mid-deploy. Use the frozen/clean variant — fast, deterministic, and fails loudly if the lockfile is stale.
  • Trusting ^1.2.3 to be safe. Caret ranges allow any minor or patch update; a malicious or buggy 1.4.0 can land via a teammate's local install. The lockfile is what actually protects you — never delete it to "clean up."
  • Switching to Bun mid-project for speed, then hitting one incompatible native module and panicking. Bun's Node compatibility is excellent but not perfect. Migrate during a quiet week, run your full test suite first, and keep your fallback (npm/pnpm) installable for a sprint.

Page checkpoint

Checkpoint Quiz

Did package managers stick?

Required

What's next

→ Continue to State Management — how your app keeps track of data and UI state across components.