What is pnpm?
pnpm is a package manager that stores every version of every package once in a global content-addressable store and hard-links it into each project that needs it. The result is dramatically less disk usage and much faster installs, especially across many projects or a monorepo.
It is also strict. Instead of flattening every dependency into one node_modules, pnpm uses symlinks that mirror the real dependency graph. A package can only import what it actually declares, so accidental reliance on a transitive dependency fails immediately instead of working locally and breaking in production.
The store and node_modules
The store lives in a global directory and holds the contents of every package version you have ever installed. When a project needs a package, pnpm hard-links it from the store rather than copying it.
The node_modules layout then uses symlinks:
node_modules/.pnpmholds the real packages.node_modules/<name>links to the version your project declared.- Each package’s own
node_moduleslinks only to its declared dependencies.
This is why pnpm catches phantom dependencies: if your code imports a package you forgot to add to package.json, it fails, because the package is not linked at the top level. npm’s flat layout often lets that mistake pass unnoticed.
Commands
The commands are close to npm’s, which makes migration easy.
pnpm install # install from the lockfile
pnpm add zod # add a dependency
pnpm add -D vitest # add a dev dependency
pnpm remove zod # remove a dependency
pnpm run build # run a script
pnpm dlx create-vite # run a package without installing
pnpm install uses the store, so repeated installs are fast. pnpm-lock.yaml plays the same role as package-lock.json and should be committed.
Workspaces
Workspaces are pnpm’s standout feature. You declare the package locations in pnpm-workspace.yaml.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Then a workspace can depend on a sibling using the workspace protocol instead of a relative file path.
{
"name": "@repo/web",
"dependencies": {
"@repo/ui": "workspace:*"
}
}
pnpm links the local package, and workspace:* is replaced with the real version when publishing. This keeps internal dependencies explicit and avoids brittle file:../.. paths.
Catalogs and version consistency
In a large monorepo it is easy for different packages to depend on different versions of the same library. Catalogs centralise that decision.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^19.0.0
typescript: ^5.6.0
{
"dependencies": {
"react": "catalog:"
}
}
Every package that uses catalog: resolves to the version defined once at the root, which makes upgrades a single edit and prevents drift.
Filtering and running tasks
pnpm can target a subset of a workspace, which is essential in a monorepo.
# run tests only in packages that changed since main
pnpm --filter "...[origin/main]" test
# run a script in one package
pnpm --filter @repo/web dev
# run a script in every package
pnpm -r build
Filters support package names, directory globs and dependency relationships, so you can run a command only where it is relevant. For caching and orchestration across packages, pair pnpm with Turborepo, which is designed for exactly this setup.
CI and reproducibility
Use a frozen lockfile in CI so the install fails if the lockfile and manifests disagree.
pnpm install --frozen-lockfile
pnpm run build
This is the pnpm equivalent of npm ci and is what guarantees a reproducible build. Because installs are fast and the store can be cached in CI, pnpm also tends to reduce pipeline time.
pnpm compared with npm
- Disk and speed: pnpm shares packages through a store and links them; npm copies a flattened tree per project.
- Strictness: pnpm exposes only declared dependencies; npm’s flat layout allows phantom imports.
- Workspaces: both support them, but pnpm’s workspace protocol and filtering are more ergonomic for large monorepos.
- Compatibility: both read
package.jsonand support the same registry, so switching is usually a matter of deletingnode_modulesand the old lockfile.
Choose npm for simplicity and ubiquity, and pnpm when disk, speed or monorepo ergonomics matter. See the npm guide for the default workflow.
Best practices
- Commit
pnpm-lock.yaml. - Use
pnpm install --frozen-lockfilein CI. - Use
workspace:*for internal dependencies. - Centralise shared versions with catalogs.
- Take advantage of strictness: add missing dependencies instead of disabling it.
- Use
--filterto run tasks only where they matter. - Pair pnpm with a task runner for caching in large repos.
Common mistakes
- Adding dependencies at the wrong workspace level with
-w. - Using
file:paths instead of the workspace protocol. - Ignoring “not declared in package.json” errors instead of fixing the manifest.
- Forgetting
--frozen-lockfilein CI and getting non-reproducible installs. - Letting each package pin its own version of a shared library.
- Mixing package managers in one repository and creating conflicting lockfiles.
Where to go next
pnpm is the efficient choice for modern JavaScript projects, especially monorepos. Compare it with npm, add Turborepo for task caching, and understand the Node.js runtime underneath. Then try it on an existing project by removing node_modules and letting pnpm rebuild the tree from the store.