Skip to content

pnpm Reference: Install, Store, Workspaces, Catalog & CI

pnpm is a fast, disk-efficient package manager. The key difference from npm and Yarn: packages are stored once in a global content-addressable store and hard-linked into projects. A 200MB package installed in 50 projects uses 200MB total, not 10GB. It’s also strict about phantom dependencies — your code can’t import a package that isn’t in your package.json, which prevents bugs that npm allows. Drop-in compatible with npm commands. The monorepo workspace support is first-class.

1. Install & Core Commands

Install, add/remove/update, filtering, and common CLI flags
# Install pnpm:
npm install -g pnpm
# Or: corepack enable && corepack use pnpm@latest

# Specify version in package.json (recommended for teams):
# "packageManager": "pnpm@9.15.0"

# Add packages:
pnpm add express         # production dependency
pnpm add -D typescript   # devDependency
pnpm add -O nodemon      # optionalDependency
pnpm add express@4.18.0  # specific version
pnpm add github:org/repo # GitHub source

# Remove:
pnpm remove express
pnpm remove -D typescript

# Install all dependencies (from lockfile):
pnpm install             # fast — uses lockfile
pnpm install --frozen-lockfile  # CI — fail if lockfile outdated

# Update packages:
pnpm update             # update to latest within semver range
pnpm update --latest    # update to absolute latest (ignores semver)
pnpm update express     # update specific package
pnpm outdated           # show which packages have new versions

# Run scripts:
pnpm run build
pnpm build              # shorthand (omit "run")
pnpm test
pnpm dev

# Execute binaries (like npx):
pnpm dlx create-next-app@latest   # equivalent to npx
pnpm exec eslint src/             # run local binary

2. Content-Addressable Store & Disk Efficiency

How pnpm’s store works, virtual store, hard links vs symlinks, and store management
# Global store location:
# macOS/Linux: ~/.local/share/pnpm/store
# Windows:     ~/AppData/Local/pnpm/store

# How it works:
# 1. First install: package files copied to global store (content-addressed by hash)
# 2. Subsequent installs: hard links from store to node_modules (no copy)
# 3. Two projects using lodash@4.17.21 share ONE copy on disk

# Check store location:
pnpm store path

# Clean up unused packages from store:
pnpm store prune    # removes unreferenced packages

# Verify store integrity:
pnpm store verify

# Virtual store — node_modules structure:
# node_modules/
#   .pnpm/          ← virtual store with all actual packages
#     express@4.18.0/node_modules/express/
#     lodash@4.17.21/node_modules/lodash/
#   express → .pnpm/express@4.18.0/node_modules/express  (symlink)
#   lodash  → (NOT here unless in your package.json)

# Phantom dependency protection:
# npm: you CAN import lodash even if it's only in express's dependencies
# pnpm: you CANNOT — only packages in YOUR package.json are accessible
# This catches bugs that npm silently allows

# .npmrc for pnpm config:
# shamefully-hoist=true   # npm-like flat node_modules (avoid if possible)
# strict-peer-dependencies=false  # disable peer dep errors
# auto-install-peers=true         # auto-install missing peer deps

3. Workspaces (Monorepo)

pnpm-workspace.yaml, filtering with –filter, and recursive commands
# pnpm-workspace.yaml — define workspace packages:
packages:
  - "apps/*"
  - "packages/*"
  - "tools/*"

# Install all workspace packages (from root):
pnpm install    # installs dependencies for all packages

# Run command in specific workspace:
pnpm --filter my-app build
pnpm --filter my-app dev
pnpm --filter "./packages/*" build   # all packages matching glob

# Run in all workspaces:
pnpm --recursive run build
pnpm -r run build           # shorthand

# Run in parallel:
pnpm --recursive --parallel run test

# Filter by dependency (build everything that depends on @my-org/ui):
pnpm --filter "...@my-org/ui" build      # @my-org/ui + all dependents
pnpm --filter "@my-org/ui..."  build     # @my-org/ui + all its dependencies

# Add dependency to specific workspace:
pnpm add react --filter my-app

# Add workspace package as dependency (local reference):
pnpm add @my-org/ui --filter my-app --workspace
# Adds: "@my-org/ui": "workspace:*" in my-app/package.json

# workspace: protocol — links local packages:
# package.json: "dependencies": { "@my-org/utils": "workspace:^1.0.0" }
# workspace:*      — always use local version
# workspace:^1.0.0 — use local version, but publish with ^1.0.0

4. Lockfile, Overrides & Catalog

pnpm-lock.yaml, overriding nested dependency versions, and the catalog feature
# pnpm-lock.yaml — always commit this file to git
# Unlike package-lock.json, pnpm's lockfile is compact and human-readable

# Check for lockfile drift:
pnpm install --frozen-lockfile   # fails if pnpm-lock.yaml is out of date
# Use this in CI — ensures reproducible installs

# Override a nested dependency version (security patches, breaking bugs):
# package.json:
{
  "pnpm": {
    "overrides": {
      "lodash": "^4.17.21",           # pin all lodash to 4.17.21+
      "axios@<1.0.0": "^1.6.0",       # only override old axios versions
      "my-app>react": "18.3.0"         # override react only inside my-app
    }
  }
}

# Catalog — share dependency versions across workspace packages:
# pnpm-workspace.yaml:
catalog:
  react: "^18.3.0"
  typescript: "^5.7.0"
  vitest: "^3.0.0"

# package.json in any workspace package:
{
  "dependencies": { "react": "catalog:" },
  "devDependencies": { "typescript": "catalog:", "vitest": "catalog:" }
}
# pnpm resolves "catalog:" to the version in pnpm-workspace.yaml
# One place to bump versions across all packages

5. CI, Docker & Migration from npm

pnpm in GitHub Actions, Docker best practices, and migrating from npm/Yarn
# GitHub Actions:
# - uses: pnpm/action-setup@v4
#   with:
#     version: 9
# - uses: actions/setup-node@v4
#   with:
#     node-version: 22
#     cache: "pnpm"
# - run: pnpm install --frozen-lockfile
# - run: pnpm test

# Docker — efficient layer caching:
FROM node:22-alpine
RUN npm install -g pnpm@9
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod   # install prod deps only
COPY . .
RUN pnpm build
CMD ["node", "dist/index.js"]

# Migration from npm:
# 1. Delete node_modules and package-lock.json
rm -rf node_modules package-lock.json
# 2. Run pnpm import (converts package-lock.json to pnpm-lock.yaml):
pnpm import            # reads package-lock.json before deleting
# 3. Install with pnpm:
pnpm install
# 4. Update scripts to use pnpm (CI, Makefile, README, Dockerfile)

# Migration from Yarn:
rm -rf node_modules yarn.lock
pnpm import   # converts yarn.lock to pnpm-lock.yaml
pnpm install

# Check for issues after migration:
pnpm audit              # security vulnerabilities
pnpm outdated           # outdated packages
pnpm list --depth 0     # top-level dependencies

Track Node.js and package manager releases at ReleaseRun. Related: Node.js Reference | Turborepo Reference | Nx Reference | Nodejs EOL Tracker

🔍 Free tool: npm Package Health Checker — check any npm/pnpm package for EOL status, known CVEs, and active maintenance before adding to your project.

Founded

2023 in London, UK

Contact

hello@releaserun.com