Skip to main content
← All Articles Β· Β· 8 min read

pnpm Workspaces & Monorepo Best Practices 2026

Master pnpm workspaces for monorepo management: setup, filtering, shared packages, changesets versioning, catalog feature, and comparison with npm/yarn workspaces.

pnpmmonorepoworkspacepackage-manager

Monorepos have become the standard architecture for teams managing multiple related packages. In 2026, pnpm Workspaces is the go-to solution β€” combining strict dependency management, fast installs via content-addressable storage, and a workspace protocol that makes cross-package development seamless. This guide covers everything from initial setup to advanced versioning with Changesets and the new Catalog feature.


Why pnpm for Monorepos

Before the setup, a quick justification. pnpm differentiates itself from npm and yarn in three ways that matter for monorepos:

  1. Hard links + content-addressable store: Packages are stored once globally, hard-linked into node_modules. A 10-package monorepo shares one copy of React on disk.
  2. Strict dependency isolation: pnpm’s node_modules layout means packages can only access what they explicitly declare β€” no phantom dependency bugs.
  3. Native workspace protocol: workspace:* references resolve at install time without publishing, unlike npm which requires published versions or file: hacks.

Setting Up a pnpm Workspace

Initialize the Monorepo

mkdir my-monorepo && cd my-monorepo
pnpm init

Create pnpm-workspace.yaml in the root:

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

Directory Structure

my-monorepo/
β”œβ”€β”€ pnpm-workspace.yaml
β”œβ”€β”€ package.json           # root β€” dev tooling only
β”œβ”€β”€ apps/
β”‚   β”œβ”€β”€ web/               # Next.js app
β”‚   └── api/               # Fastify API
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ ui/                # Shared component library
β”‚   β”œβ”€β”€ utils/             # Shared utilities
β”‚   └── config/            # ESLint/TS configs
└── tools/
    └── scripts/           # Monorepo maintenance scripts

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "engines": {
    "node": ">=20",
    "pnpm": ">=9"
  },
  "scripts": {
    "build": "pnpm -r build",
    "dev": "pnpm --filter web dev",
    "test": "pnpm -r test",
    "lint": "pnpm -r lint",
    "format": "prettier --write ."
  },
  "devDependencies": {
    "prettier": "^3.0.0",
    "typescript": "^5.0.0"
  }
}

Cross-Package Dependencies

Use workspace:* to reference local packages:

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@my-monorepo/ui": "workspace:*",
    "@my-monorepo/utils": "workspace:*"
  }
}

pnpm resolves workspace:* to the current local version at install and rewrites it to the actual version on publish.


Filtering: Target Specific Packages

pnpm’s --filter flag is one of its killer features for monorepos.

Basic Filtering

# Run build in a specific package
pnpm --filter web build

# Run in all packages matching glob
pnpm --filter '@my-monorepo/*' build

# Run in all packages with a specific script
pnpm -r --if-present test

Dependency-Aware Filtering

# Build the ui package and everything that depends on it
pnpm --filter '...@my-monorepo/ui' build

# Build ui and all its dependencies first
pnpm --filter '@my-monorepo/ui...' build

# Since a file changed (git-based)
pnpm --filter '[origin/main]' build

The [origin/main] filter is especially useful in CI β€” it only runs tasks for packages affected by changes since the last merge.

Parallel Execution

# Build all packages in parallel (respects dependency graph)
pnpm -r --parallel build

# Stream output for easier debugging
pnpm -r --stream build

Shared Packages

Shared UI Components

# packages/ui/
pnpm init
// packages/ui/package.json
{
  "name": "@my-monorepo/ui",
  "version": "0.1.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "dev": "tsup src/index.ts --watch"
  }
}

Shared ESLint Config

// packages/config/eslint-base.js
module.exports = {
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
  rules: {
    'no-console': 'warn',
  },
}
// apps/web/.eslintrc.json
{
  "extends": ["@my-monorepo/config/eslint-base"]
}

TypeScript Project References

For cross-package type checking without building:

// tsconfig.json (root)
{
  "references": [
    { "path": "./packages/ui" },
    { "path": "./packages/utils" },
    { "path": "./apps/web" }
  ]
}
// packages/ui/tsconfig.json
{
  "extends": "../../packages/config/tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Versioning with Changesets

Changesets is the standard versioning tool for pnpm monorepos.

Setup

pnpm add -D @changesets/cli -w
pnpm changeset init

Workflow

# Developer: add a changeset after making changes
pnpm changeset

# Select packages affected and bump type (major/minor/patch)
# Write a human-readable changelog entry
# Commit the .changeset/*.md file
# Release manager: version all packages
pnpm changeset version

# This updates package.json versions and CHANGELOG.md files
# Commit the version bumps

# Publish all changed packages
pnpm changeset publish

Automated Releases via CI

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm install --frozen-lockfile
      - uses: changesets/action@v1
        with:
          publish: pnpm changeset publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

The Catalog Feature (pnpm 9+)

The Catalog feature (introduced in pnpm 9) lets you define shared dependency versions in pnpm-workspace.yaml, eliminating version drift across packages.

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

catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.4.0
  zod: ^3.22.0
  '@tanstack/react-query': ^5.0.0

Reference catalog versions in any package:

// apps/web/package.json
{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:",
    "@tanstack/react-query": "catalog:"
  }
}

When you run pnpm catalog update react, it bumps the version in pnpm-workspace.yaml and all packages that reference it simultaneously β€” no more hunting for version mismatches.

Named Catalogs

For projects with multiple React versions (e.g., legacy and modern):

catalogs:
  react18:
    react: ^18.3.0
    react-dom: ^18.3.0
  react19:
    react: ^19.0.0
    react-dom: ^19.0.0
{
  "dependencies": {
    "react": "catalog:react19"
  }
}

pnpm vs npm vs Yarn Workspaces

FeaturepnpmnpmYarn (Berry)
Disk usageBest (hard links)HighMedium (PnP)
Install speedFastestSlowestFast (cache)
--filter supportExcellentBasicGood
Catalog featureYes (v9+)NoNo
Plug’n’PlayNoNoYes (Berry)
Phantom depsBlockedAllowedBlocked (PnP)
Ecosystem compatExcellentExcellentMixed (PnP)

npm workspaces work fine for small monorepos but lack filtering power and allow phantom dependencies.

Yarn Berry with PnP eliminates node_modules entirely but suffers ecosystem compatibility issues β€” many tools still assume the node_modules structure.

pnpm hits the sweet spot: strict isolation without PnP compatibility headaches, the fastest installs, and the most powerful workspace tooling.


Practical Tips

Lock Node and pnpm Versions

// package.json
{
  "engines": {
    "node": ">=20.11.0",
    "pnpm": ">=9.0.0"
  },
  "packageManager": "[email protected]"
}

Pre-install Hook

Prevent accidental npm/yarn installs:

# .npmrc
engine-strict=true
// scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
  console.error('Use pnpm to install dependencies.')
  process.exit(1)
}

turbo for Task Orchestration

For large monorepos, combine pnpm workspaces with Turborepo for task caching:

pnpm add -D turbo -w
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

Key Takeaways

  • pnpm workspaces with pnpm-workspace.yaml is the fastest monorepo setup in 2026
  • workspace:* protocol handles cross-package references without publishing
  • --filter with dependency-awareness (...package / package...) makes CI surgical
  • Changesets is the standard for versioning and changelog management
  • Catalog (pnpm 9+) solves version drift across packages declaratively
  • pnpm beats npm and Yarn workspaces on speed, strictness, and tooling maturity
Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.