Context
Every npm install (or pnpm install) hides an architecture decision most developers never stop to think about: where, on disk, the downloaded packages actually live. npm, since the rewrite of its dependency tree a few years ago, installs packages in a flat (hoisted) structure: most dependencies, direct and transitive, end up at a single level inside node_modules, which resolves duplicate-version conflicts at the cost of letting a package access another one it never declared as a dependency. pnpm attacks the same problem from a different angle: it keeps a global content-addressable store on disk, with a hash per package version, and assembles each project's node_modules using hardlinks and symlinks into that store — which saves space when the same package is used across several projects, and produces a non-flat tree where only the dependencies actually declared are accessible.
npm's structural advantage is availability: it already comes bundled with every Node.js install, so there's no extra setup step. pnpm's structural advantage is how much better it scales as the number of projects or the size of a monorepo grows, both in disk usage and install time, because packages already downloaded once into the store never need to be downloaded or copied again.
When to choose pnpm
pnpm pays off when the team maintains multiple projects on the same disk, or a monorepo with many internal packages, and feels the disk and time cost that a flat structure with full copies per project imposes. Workspaces and filters are native:
pnpm install
pnpm --filter ./packages/api build
--filter lets you run commands only on the affected packages of a monorepo, without depending on an external orchestration tool for that. On the correctness side, the store's non-flat structure has a welcome side effect: a package can't silently import another package that was never declared as a direct dependency, because it simply doesn't exist in the project's local node_modules — a phantom-dependency bug that npm's flat structure lets slip through unnoticed until it breaks in another environment. The trade-off is that pnpm needs to be installed separately — via corepack, its own install script, or the operating system's package manager — before any pnpm install will work.
When to choose npm
npm pays off when the team wants the lowest-friction path possible: downloading Node.js already brings npm along, with no additional tooling decision to make:
npm install
npm run build --workspaces
npm's native workspaces cover the common monorepo case — several package folders managed by a single lockfile at the root — without needing to install anything beyond what's already available. For small or medium projects, without multiple repositories sharing the same dependencies on the same disk, the extra space cost of the flat structure is rarely noticeable day to day, and the absence of a separate install step is one less source of friction when onboarding new developers or in CI environments that already come with Node.js pre-installed.
Verdict
In a large monorepo, or on a development machine with dozens of JavaScript projects, the disk cost and install time of npm's flat model show up fast — and that's the scenario where switching package managers stops being a team preference and becomes an infrastructure decision. A single project, with few contributors and no other repositories sharing the same dependencies on disk, is unlikely to feel that difference, since both managers produce a working tree in the overwhelming majority of projects. What matters most, then, is where on that growth curve your development environment actually sits.