← All articles
JAVASCRIPT pnpm Workspaces: Monorepo Management Without the Ove... 2026-03-04 · 4 min read · pnpm · workspaces · monorepo

pnpm Workspaces: Monorepo Management Without the Overhead

JavaScript 2026-03-04 · 4 min read pnpm workspaces monorepo package manager npm typescript bun turborepo

Monorepos — multiple packages in one repository — solve the problem of managing shared code across related projects. pnpm workspaces make this practical with one of the fastest package managers and a symlink-based approach that dramatically reduces disk usage and install times.

If you're managing a frontend app, backend API, and shared types in separate repos and constantly fighting version mismatches, pnpm workspaces are worth understanding.

Why pnpm for Workspaces

Speed: pnpm is significantly faster than npm and yarn for installs, especially in monorepos with many packages.

Disk efficiency: pnpm uses a content-addressable store and hard links. If 100 packages all depend on lodash 4.17, only one copy of lodash exists on disk. npm and yarn create separate copies in each node_modules.

Strict by default: pnpm prevents accessing packages that aren't listed in your package.json. This prevents "phantom dependencies" — accidentally using packages that aren't explicitly declared.

Native workspace protocol: workspace:* references work reliably across pnpm, without the quirkiness of npm/yarn workspace handling.

Basic Workspace Setup

Repository structure:

my-app/
├── package.json        # root workspace config
├── pnpm-workspace.yaml
├── packages/
│   ├── web/            # Next.js frontend
│   │   └── package.json
│   ├── api/            # Express backend
│   │   └── package.json
│   └── types/          # Shared TypeScript types
│       └── package.json
└── apps/               # optional second category

Root pnpm-workspace.yaml:

packages:
  - 'packages/*'
  - 'apps/*'

Root package.json:

{
  "name": "my-monorepo",
  "private": true,
  "engines": {
    "node": ">=20",
    "pnpm": ">=9"
  },
  "scripts": {
    "dev": "pnpm --parallel -r dev",
    "build": "pnpm -r build",
    "test": "pnpm -r test",
    "lint": "pnpm -r lint"
  }
}

Package Setup with workspace: Protocol

In packages/web/package.json:

{
  "name": "@my-org/web",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "@my-org/types": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  }
}

workspace:* tells pnpm to use the local workspace package instead of fetching from npm. This is the key to cross-package references in a monorepo.

In packages/types/package.json:

{
  "name": "@my-org/types",
  "version": "0.0.1",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "devDependencies": {
    "typescript": "^5"
  }
}

TypeScript Configuration for Monorepos

Create a base tsconfig.json at the root:

// tsconfig.json (root)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

Each package extends it:

// packages/web/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

TypeScript project references (optional but recommended for type-checking performance):

// tsconfig.json (root)
{
  "files": [],
  "references": [
    { "path": "./packages/types" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

With project references, tsc --build builds packages in dependency order, and type checking is incremental.

Running Commands

Install all dependencies:

pnpm install  # from root

Run a script in all packages:

pnpm -r build       # build all packages
pnpm -r --parallel dev  # run dev in all packages concurrently

Run a script in a specific package:

pnpm --filter @my-org/web dev
pnpm --filter @my-org/web build

Run in a package directory:

cd packages/web && pnpm dev
# or:
pnpm --filter @my-org/web run dev

Add a dependency to a specific package:

pnpm --filter @my-org/web add react-query
pnpm --filter @my-org/api add --save-dev @types/node

Add a dependency to the root (dev tools shared across all packages):

pnpm add -w -D typescript prettier

Shared Dev Tools

Put dev tools used across packages at the root level:

// package.json (root)
{
  "devDependencies": {
    "typescript": "^5",
    "prettier": "^3",
    "eslint": "^8",
    "turbo": "^2"  // if using Turborepo
  }
}

Packages import root-level tools automatically due to pnpm's hoisting behavior. Override the hoist behavior in .npmrc:

hoist-pattern[]=*

Turborepo Integration

For large monorepos, add Turborepo on top of pnpm workspaces for caching and build graph optimization:

pnpm add -w -D turbo
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

Turborepo caches task outputs and only re-runs tasks when inputs change. On CI, this means a turbo build only rebuilds packages that have changed since the last build.

pnpm Workspaces vs. npm/yarn Workspaces

Feature pnpm npm yarn (berry)
Speed Fast Slow-moderate Fast
Disk usage Very efficient (hard links) Per-package node_modules Zero-install (PnP)
Strict dependencies Yes No Yes
workspace: protocol Yes No Yes
Plug'n'Play No No Yes (default in v2+)
Compatibility High Full Mixed (PnP has issues)

pnpm is the recommended choice for new monorepos. npm workspaces work but are slower. Yarn Berry's Plug'n'Play mode is powerful but has compatibility issues with some tools.

Common Monorepo Patterns

Shared config package:

packages/
  config/
    src/
      eslint.js
      tsconfig.json
      vitest.config.ts
    package.json

Each package imports config from @my-org/config and extends it.

Internal packages that shouldn't be published: Add "private": true to their package.json. pnpm won't publish them.

Version management: For apps where packages don't need semver (internal only), use "version": "0.0.0" as a placeholder. For packages that get published, use Changesets.

.npmrc Configuration

# .npmrc
link-workspace-packages=true   # link workspace deps by default
hoist-pattern[]=*              # hoist all packages to root node_modules
shamefully-hoist=false         # keep strict (don't ghost deps into apps)
strict-peer-dependencies=false # less noisy about peer deps in dev

pnpm workspaces scale from small 2-3 package repos to large codebases with hundreds of packages. The combination of speed, disk efficiency, and strict dependency resolution makes it the package manager of choice for serious monorepo setups.