How pnpm Workspaces Keep Monorepo Dependencies Isolated
pnpm workspaces avoid the node_modules bloat and phantom dependencies of traditional monorepo installs by using a shared content-addressable store and symlinks. Here is how the model works and how to verify it.
04 Aug 2026, 18:52 UTC

The monorepo install that eats your disk
When you split an application into multiple packages inside one repository, the default installers tend to flatten everything. npm and yarn hoist dependencies to the root node_modules so that every package can resolve them. That sounds convenient until you notice two problems: the root directory grows because each package keeps its own copy of shared libraries, and packages can import dependencies they never declared in their package.json. Those undeclared imports are called phantom dependencies, and they break silently when a sibling package is removed or upgraded.
In a large monorepo, the duplication is not just a nuisance. Ten packages depending on the same utility library can mean ten copies on disk with npm, and a single version bump can trigger a cascade of reinstalls across the tree. pnpm workspaces exist to change that math.
What pnpm workspaces actually do
pnpm approaches the same monorepo layout with a different storage model. You declare workspace directories in the root package.json under pnpm.workspaces. When you run pnpm install from the repository root, pnpm builds a content-addressable store inside .pnpm. A content-addressable store keeps one physical copy of each package version, keyed by its content hash. If three workspace packages all depend on the same version of a library, the store holds it once.
Instead of copying files into every package, pnpm creates symlinks. Each workspace package gets a node_modules directory whose entries point back into the shared .pnpm store. By default, pnpm does not hoist those links to the repository root. The dependencies of packages/api stay isolated from the dependencies of packages/web, which prevents the bloat and phantom imports common in hoisted layouts.
Linking a new workspace package
Suppose you want to extract a shared utility into its own workspace package. The following commands are run from the repository root and require only normal write access to the project directory:
mkdir -p packages/util
cd packages/util
pnpm init -y
pnpm add lodash
That creates a new package and installs lodash into it. Because the directory matches a pattern in pnpm.workspaces, pnpm treats it as a workspace member rather than a nested project.
To make that package available to the rest of the monorepo, return to the root and add it as a dependency:
cd /path/to/repo/root
pnpm add -w util
The -w flag targets the workspace root manifest. pnpm recognizes util as a local workspace package, so it writes a workspace-relative symlink instead of downloading anything from a registry. Other packages can depend on util the same way, and they will resolve to the same linked directory.
Checking that the link is real
Two quick checks confirm the setup. First, list the registered workspaces:
pnpm workspaces ls
Expected output includes packages/util alongside your other workspace directories. Second, inspect the symlink structure from the root:
ls -la node_modules | head -20
You should see symbolic links pointing into .pnpm/store/<hash>/ rather than real directories. If you see ordinary folders, the install likely ran in the wrong directory or the workspace glob did not match.
Sharp edges worth planning for
The isolation model has trade-offs. The most common mistake is committing node_modules to version control. Workspace symlinks are absolute or relative paths that depend on the local store layout; once committed, they break on every clone. Add node_modules to .gitignore and rely on the lockfile plus pnpm install to rebuild the tree.
Peer dependencies can also behave differently in a workspace. Because packages are not hoisted by default, a peer dependency that is missing from a specific package may produce a warning. The flag --no-strict-peer-dependencies can silence the error, but use it sparingly: it hides a real version mismatch rather than fixing it.
Finally, resolutions in the root package.json apply globally across every workspace package. If you need to override a version for only one package, scope the resolution per package instead of placing it at the root.
Filtering commands in CI
In a monorepo, you rarely want to build or test everything on every commit. The --filter flag restricts a command to matching workspace packages:
pnpm --filter "./packages/**" test
Run this from the repository root. It executes the test script only in packages that match the glob, which keeps pipeline runtimes proportional to what actually changed. The same pattern works for build, lint, or any script defined in the workspace manifests.
Bottom line
pnpm workspaces trade the convenience of a flat, hoisted node_modules for explicit isolation and lower disk usage. The model works best when you treat node_modules as disposable, verify workspace registration with pnpm workspaces ls, and use --filter to scope automation. If your monorepo has outgrown duplicated installs or phantom imports, that trade is usually worth making.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.