Skip to content
Back to Knowledge Base

pnpm Workspaces: Setup, Commands, and Best Practices

pnpm workspaces let you develop multiple packages in a single repository (or a monorepo). You declare package locations in pnpm-workspace.yaml, link local packages with the workspace: protocol, and pnpm installs everything into one content-addressable store, so each package gets a strict node_modules without duplicating dependencies on disk.

For the full configuration reference, see the pnpm workspaces documentation.

A pnpm workspace is any repository with a pnpm-workspace.yaml file at its root. Add it next to a root package.json marked "private": true so the root is never published by accident. The packages field lists the directories that hold package.json files:

pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'

Every directory matched by those globs that contains a package.json becomes a workspace package. A common layout separates applications from shared packages:

  • Directorymy-workspace/
    • Directoryapps/
      • Directoryweb/
        • package.json
    • Directorypackages/
      • Directoryshared-ui/
        • package.json
    • package.json
    • pnpm-workspace.yaml
Section titled “3. Link packages with the workspace: protocol”

To depend on another package in the workspace, reference it with the workspace: protocol so pnpm always resolves it to the local package:

apps/web/package.json
{
"name": "web",
"dependencies": {
"react": "^19.0.0",
"@my-workspace/shared-ui": "workspace:*",
},
}

The suffix controls what pnpm publish writes into the published package.json. With the workspace at version 1.5.0, workspace:* becomes the exact version 1.5.0, workspace:^ becomes ^1.5.0, and workspace:~ becomes ~1.5.0. During local development all of them use the local package.

Run pnpm install once at the root to install every package's dependencies and link the internal ones.

pnpm -r run build runs the build script in every package. The --filter flag narrows that to a subset, and it understands the workspace's dependency relationships:

Terminal window
pnpm --filter web run build # a single package
pnpm --filter "web..." run build # web and everything it depends on
pnpm --filter "...shared-ui" run test # shared-ui and everything that depends on it

Filters also select packages by what changed in git. In CI, test only the packages a PR touches plus their dependents:

Terminal window
pnpm --filter "...[origin/main]" run test

[origin/main] selects packages with changes since that ref, and the leading ... adds their dependents.

Share dependency versions with pnpm catalogs

Section titled “Share dependency versions with pnpm catalogs”

Catalogs (pnpm 9.5+) define a dependency version once in pnpm-workspace.yaml and reference it everywhere, so packages can't drift onto different versions of the same dependency:

pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
catalog:
react: ^19.0.0
react-dom: ^19.0.0
apps/web/package.json
{
"name": "web",
"dependencies": {
"react": "catalog:",
},
}

Upgrading React across the whole repository is now a one-line change, and version bumps stop producing merge conflicts in every package.json.

Do pnpm workspaces replace a monorepo tool?

Section titled “Do pnpm workspaces replace a monorepo tool?”

No. pnpm workspaces handle installing and linking packages, and that's where they stop. There's no task graph and no caching, and git-based --filter compares changed files rather than task inputs, so CI still reruns tasks a change couldn't have affected.

When task time becomes the bottleneck, add Nx on top without changing how pnpm works:

Terminal window
npx nx@latest init

Your existing package.json scripts keep working and pnpm keeps managing installs.

Before Nx, pnpm runs a script across packages, but it runs every task every time and has no notion of which packages a change affects:

Terminal window
pnpm -r run build # every package
pnpm --filter web run build # a single package

After nx init, run the same scripts through Nx:

Terminal window
nx run-many -t build # every project
nx build web # a single project
nx affected -t build # only projects touched by your changes

The first run executes your scripts. A second run with no changes is restored from the cache, and nx affected skips the projects your change does not touch.

nx init writes an nx.json with targetDefaults that control how targets behave:

nx.json
{
"targetDefaults": {
"build": {
"cache": true,
"dependsOn": ["^build"],
},
"dev": {
"continuous": true,
},
},
}

cache: true makes a target cacheable: Nx hashes each project's inputs (source files, dependencies, and config) and restores its outputs from the cache when nothing has changed. Nothing is cached unless you opt in, so you stay in control. dependsOn: ["^build"] builds a project's dependencies first. continuous: true marks tasks that don't exit, like dev servers and watchers, so tasks that depend on them don't wait for them to complete.

Nx Cloud adds more ways to speed up CI for a pnpm workspace: remote caching shares the cache across CI runs and teammates, Nx Agents distribute tasks across machines, and self-healing CI proposes fixes when tasks fail. For the full walkthrough, see Adding Nx to an existing monorepo and CI setup, or the From pnpm Workspaces to Distributed CI course.

Working with a different package manager? See npm workspaces, Yarn workspaces, or Bun workspaces. New to monorepos? Start with what a monorepo is and why teams use one.

Last updated: