Picture a laptop that has hosted a dozen JavaScript projects. The same version of Express sits in a dozen node_modules folders. It works, and on a fast connection nobody notices, until the disk fills up or a CI runner installs everything from scratch on every push. The waste is not dramatic, it is steady: a few minutes here, a few gigabytes there, and a node_modules folder that becomes the biggest thing on the machine. pnpm goes after that waste. It keeps one copy of each package version in a shared store and links it into every project that asks for it. Is that saving worth changing how installs work?
What I would check before using it
My first check is how many projects share packages on the same machine. One project with a handful of dependencies gains almost nothing from it. The saving shows up with several projects, a monorepo, or a CI runner that installs fresh each build. If that is your setup, the store earns its place; if not, stay with npm.
My second check is whether the code uses packages it never declared. npm lays dependencies out flat, so a project can require a package that arrived only as a side effect of something else, and it works by accident. pnpm keeps node_modules strict: only packages listed in package.json are visible at the top level. That strictness catches hidden assumptions before they break a deploy, but the first switch can surface missing declarations to fix, so allow time for cleanup.
My third check is peer dependencies. pnpm is stricter about them too, and warns or fails where npm silently picked a version. That forces version conflicts into the open, but a tangled peer tree will need them resolved instead of ignored.
My fourth check is the team and its tooling. pnpm reads the same registry and package.json, and produces a lockfile, pnpm-lock.yaml, that plays the same role as package-lock.json. Most tools work fine with it, but anything assuming a flat node_modules layout, or a script that runs npm install directly, needs updating. Check the CI config first.
My verdict, as judgement: use pnpm when several projects share dependencies, or when you want dependency lists you can trust. Skip it for a single small project where npm works. The recommendation flips if the team leans on undeclared packages with no time to clean them up, or if a pipeline tool cannot handle pnpm's layout.
Two projects, one shared copy of Express
The setup is two small projects on the same machine, site-a and site-b, each a plain Express app with exactly one dependency in package.json:
{ "name": "site-a", "version": "1.0.0", "dependencies": { "express": "4.21.2" } } Both were installed with pnpm 10.14.0 on Node v24.22.1. The first install in site-a downloaded Express and its 68 transitive dependencies, 69 packages in total, and finished in under four seconds:
Packages: +69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dependencies: + express 4.21.2 (5.3.0 is available) Done in 3.8s using pnpm v10.14.0 The second install, in site-b with the identical dependency, also finished in 3.8 seconds, and it did not need a second download, because the packages were already in the store. The store lives at a single path on this machine, /home/hatch/.local/share/pnpm/store/v10, and it held one 4.0M copy of everything both projects needed. Inside each project, node_modules/express is not a copy at all, it is a symlink:
node_modules/express -> .pnpm/express@4.21.2/node_modules/express A plain du comparison looks confusing at first: du reported 4.5M for each pnpm node_modules folder and 3.9M for an npm install of the same Express version, with an apparent size of 2.2M in all three cases. The numbers are noisy because pnpm links files from the store with hard links, and du counts a hard-linked file once per folder it appears in. The honest measure is the store itself: one 4.0M copy served both projects, while npm copied the files into each project separately. The saving grows with every extra project that shares the same versions.
To prove the install actually worked, site-a runs a tiny server:
const express = require('express'); const app = express(); app.get('/', (req, res) => { res.json({ message: 'Hello from site-a', packages: 'express 4.21.2 via pnpm' }); }); app.listen(3456, () => console.log('Listening on port 3456')); A curl request to the running server returned the JSON exactly as written:
{"message":"Hello from site-a","packages":"express 4.21.2 via pnpm"} The thing that went wrong came from that strict node_modules layout. Express itself depends on a package called debug, so debug is present on disk inside node_modules/.pnpm, but it is not listed in site-a's package.json. Requiring it directly failed:
Error: Cannot find module 'debug' Require stack: - /home/hatch/workspace/.toolchains/verify/pnpm-demo/site-a/[eval] at Module._resolveFilename (node:internal/modules/cjs/loader:1564:15) at wrapResolveFilename (node:internal/modules/cjs/loader:1118:27) at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1142:10) at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1169:12) at Module._load (node:internal/modules/cjs/loader:1341:5) at wrapModuleLoad (node:internal/modules/cjs/loader:261:19) at Module.require (node:internal/modules/cjs/loader:1674:12) at [eval]:1:1 at runScriptInThisContext (node:internal/vm:219:10) { code: 'MODULE_NOT_FOUND', requireStack: [ '/home/hatch/workspace/.toolchains/verify/pnpm-demo/site-a/[eval]' ] } Node.js v24.22.1 The same require in the npm comparison project loaded debug without complaint, version 2.6.9, because npm's flat layout leaves every transitive package within reach. That is the accident pnpm refuses to repeat: code that depends on debug should say so. The fix is to declare it, then install again:
"dependencies": { "express": "4.21.2", "debug": "4.3.7" } After pnpm install reported "Packages: +1", the same require worked and reported version 4.3.7, the version actually declared, rather than the older 2.6.9 that Express happens to use internally. The error felt annoying in the moment, and it was doing exactly what the tool promises: the dependency list now says what the code really needs.
Common mistakes and how to fix them
First, the phantom dependency above. The symptom is Error: Cannot find module 'debug' with code MODULE_NOT_FOUND, for a package sitting under node_modules. The fix is to declare the package in package.json and run pnpm install, never reach into .pnpm by hand.
Second, a lockfile mismatch in CI. The symptom is a failed install saying "specifiers in the lockfile don't match specifiers in package.json" and a line like "express (lockfile: 4.21.2, manifest: 4.21.1)". The fix is to run pnpm install locally so the lockfile updates, then commit it; never hand-edit a version without reinstalling.
Third, mixing npm and pnpm in one project. The symptom is two lockfiles, package-lock.json beside pnpm-lock.yaml, and installs that behave differently on different machines. The fix is to delete package-lock.json and node_modules, run pnpm install, and keep only one lockfile in the repo.
Fourth, judging the store by du on node_modules. The symptom is a node_modules folder that still reports several megabytes, as if the saving never happened. The fix is to measure the store path with du instead, since hard links make per-project folders look bigger than the disk they use.
My call on pnpm
pnpm earns its place on a machine with several JavaScript projects, or a team that wants dependency lists it can trust. The shared store is a real saving, and the strict layout turns silent accidents into loud, fixable errors at install time instead of on a deploy. For one small project with npm already working, switching buys little and adds a new tool to learn. If your setup looks like the two projects above, try it on one of them first: install, run the server, and see whether anything in the code was quietly depending on a package it never declared. If the list comes back clean, keep going.
Further reading
- Why pnpm, https://pnpm.io/motivation
- Installing pnpm, https://pnpm.io/installation
- pnpm add, https://pnpm.io/cli/add
- The npm package manager, Node.js docs, https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager
Originally published at https://practicaldevblog.blogspot.com/2026/11/pnpm-one-copy-of-every-package-shared.html



