pnpm vs npm workspaces: which should you use for a monorepo?
Use pnpm once your monorepo has more than a handful of packages; npm workspaces is the right call for a small repo where you'd rather not add another tool. The deciding factor isn't install speed, it's whether you want strict dependency isolation. pnpm gives every package only the dependencies it actually declared, so a package can't accidentally import something a sibling installed. npm workspaces hoists everything into one root node_modules, which is simpler until a phantom-dependency bug ships to production because a package used a library it never listed. pnpm also stores one copy of each package version in a global content-addressable store and hard-links it into each project, so a ten-package repo doesn't keep ten copies of React on disk. npm workspaces has shipped since npm v7 (October 2020) and covers the basics: shared install, symlinked local packages, one lockfile. It just stops there.
Where pnpm pulls ahead
Three things, in order of how often they actually matter:
- Strictness. pnpm's
node_modulesis a symlink tree where each package sees only its declared deps. The class of bug where CI passes and prod breaks because of an undeclared dependency mostly disappears. This is the reason Vue, Vite, Nuxt, Astro, and Turborepo all default to pnpm, and the Vue team named it directly when they moved off Yarn. - Filtering.
--filterscopes any command to one package and, optionally, everything that depends on it. Building or testing just the thing you touched is a one-liner. - Disk. The content-addressable store means a second project reusing the same dependency versions installs almost instantly and adds close to zero disk. On a large monorepo this is the difference between a few hundred megabytes and a few gigabytes.
What that looks like
// pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
# reference a local package by its workspace version
# package.json -> "dependencies": { "@acme/ui": "workspace:*" }
# run one package's build, plus everything downstream of it
pnpm --filter @acme/ui... build
When npm workspaces is the better choice
If your repo is two or three packages, everything is first-party, and you don't have a phantom-dependency problem, npm workspaces costs you nothing and adds no tool to onboard. It reads a workspaces array in the root package.json, links local packages, and shares one lockfile. Reach for pnpm when install time starts to hurt, when a hoisting bug bites, or when you want per-package task filtering. Not before.
Switching is cheap
Both resolve the same package.json semantics, so the move is mechanical: delete node_modules and package-lock.json, add a pnpm-workspace.yaml listing your package globs, replace "*" local versions with workspace:*, and run pnpm install. Budget an afternoon for a mid-size repo, mostly spent on scripts that assumed a hoisted node_modules layout.
rm -rf node_modules package-lock.json
# add pnpm-workspace.yaml, then:
pnpm install
pnpm --filter web dev
Notes from building tooling at fernforge. Mechanism details per pnpm.io; npm workspaces shipped in npm v7 (2020).