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.
Set up a pnpm workspace
Section titled “Set up a pnpm workspace”1. Create pnpm-workspace.yaml
Section titled “1. Create pnpm-workspace.yaml”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:
packages: - 'apps/*' - 'packages/*'2. Add packages
Section titled “2. Add 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
3. Link packages with the workspace: protocol
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:
{ "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.
Run scripts across packages with --filter
Section titled “Run scripts across packages with --filter”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:
pnpm --filter web run build # a single packagepnpm --filter "web..." run build # web and everything it depends onpnpm --filter "...shared-ui" run test # shared-ui and everything that depends on itFilters also select packages by what changed in git. In CI, test only the packages a PR touches plus their dependents:
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:
packages: - 'apps/*' - 'packages/*'catalog: react: ^19.0.0 react-dom: ^19.0.0{ "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:
npx nx@latest initYour existing package.json scripts keep working and pnpm keeps managing installs.
Run tasks before and after Nx
Section titled “Run tasks before and after Nx”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:
pnpm -r run build # every packagepnpm --filter web run build # a single packageAfter nx init, run the same scripts through Nx:
nx run-many -t build # every projectnx build web # a single projectnx affected -t build # only projects touched by your changesThe 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.
Configure caching and dev servers
Section titled “Configure caching and dev servers”nx init writes an nx.json with targetDefaults that control how targets behave:
{ "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.
Speed up CI with Nx Cloud
Section titled “Speed up CI with Nx Cloud”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.