DevToolBoxFREE
Blog

pnpm Complete Guide: Fast, Disk-Efficient Package Manager (2026)

18 min readby DevToolBox Team

pnpm is a fast, disk space efficient package manager for Node.js that uses a content-addressable store and hard links to save disk space and speed up installations. Unlike npm and Yarn, pnpm creates a non-flat node_modules structure that enforces strict dependency resolution, preventing phantom dependencies and ensuring your project only accesses packages it explicitly declares. With built-in monorepo support through workspaces, patching capabilities, and excellent CI/CD integration, pnpm has become the package manager of choice for many production teams and open source projects.

TL;DR

pnpm is a Node.js package manager that uses a global content-addressable store with hard links, saving up to 70% disk space compared to npm. It creates a strict non-flat node_modules layout that prevents phantom dependencies, supports monorepos via pnpm-workspace.yaml, offers up to 2x faster installs than npm, and provides features like package patching, overrides, and side-by-side protocol for linking local packages.

Key Takeaways
  • pnpm uses a content-addressable store where every version of every package is stored only once on disk, saving massive amounts of space across projects.
  • The strict non-flat node_modules structure prevents phantom dependencies, meaning your code cannot accidentally import packages not declared in package.json.
  • pnpm workspaces provide first-class monorepo support with pnpm-workspace.yaml, workspace protocol, and catalogs for shared dependency versions.
  • Installation speeds are up to 2x faster than npm and comparable to Yarn, especially on CI with warm caches.
  • The pnpm patch command lets you apply patches to third-party packages without forking, tracked in your repository via patchedDependencies.
  • pnpm is fully compatible with npm registries and can be activated via Corepack, the built-in Node.js package manager manager.

What Is pnpm and Content-Addressable Storage?

pnpm stands for performant npm. It was created in 2017 by Zoltan Kochan to solve two fundamental problems with npm: wasted disk space from duplicated packages across projects and the flat node_modules structure that allows phantom dependencies. pnpm introduces a fundamentally different approach to package management by using a global content-addressable store combined with hard links and symlinks.

When you install a package with pnpm, the package files are stored in a single global store (typically at ~/.local/share/pnpm/store or ~/.pnpm-store). Each file is stored by its content hash, so identical files across different package versions are stored only once. Your project node_modules directory contains hard links to the store rather than copies of the actual files. This means if you have 10 projects using React 19, the React files exist only once on disk.

The node_modules layout created by pnpm is non-flat. Instead of hoisting all dependencies to the root level like npm, pnpm uses a nested structure with symlinks. Only the packages listed in your package.json are directly accessible. Transitive dependencies are stored in a hidden .pnpm directory and linked from there. This strict isolation prevents your code from importing packages it does not explicitly depend on.

# How pnpm node_modules structure works
#
# node_modules/
#   .pnpm/
#     react@19.0.0/
#       node_modules/
#         react/        -> hard links to store
#     lodash@4.17.21/
#       node_modules/
#         lodash/       -> hard links to store
#   react/              -> symlink to .pnpm/react@19.0.0/node_modules/react
#   lodash/             -> symlink to .pnpm/lodash@4.17.21/node_modules/lodash
#
# Only react and lodash are visible at the top level
# because they are in your package.json.
# Transitive dependencies are isolated inside .pnpm/

pnpm vs npm vs Yarn vs Bun

Each Node.js package manager has different trade-offs in speed, disk usage, strictness, and ecosystem support. Here is how pnpm compares to the alternatives across key dimensions:

FeaturepnpmnpmYarn (Berry)Bun
Disk usageMinimal (content-addressable store + hard links)High (full copies per project)Low with PnP (no node_modules)High (full copies per project)
Install speedFast (2x npm on average)BaselineFast with PnP, moderate with node_modulesVery fast (native runtime)
StrictnessStrict (no phantom deps)Loose (flat hoisting)Strict with PnP, loose with node_modulesLoose (flat hoisting)
Monorepo supportBuilt-in workspaces + catalogsBasic workspacesBuilt-in workspaces + constraintsBasic workspaces
Lockfilepnpm-lock.yaml (YAML)package-lock.json (JSON)yarn.lock (custom format)bun.lock (binary)
PatchingBuilt-in pnpm patchRequires patch-packageBuilt-in yarn patchBuilt-in bun patch

Installation and Setup with Corepack

The recommended way to install pnpm is through Corepack, a tool included with Node.js 16.13+ that manages package manager versions. Corepack ensures every developer on your team uses the exact same pnpm version without manual installation.

You can also install pnpm standalone using npm, Homebrew, or a direct install script. Once installed, pnpm uses the same npm registry and respects .npmrc configuration files.

# Method 1: Corepack (recommended)
corepack enable
corepack prepare pnpm@latest --activate

# Add to package.json for team-wide version pinning
# "packageManager": "pnpm@10.5.0"

# Method 2: npm
npm install -g pnpm

# Method 3: Homebrew (macOS)
brew install pnpm

# Method 4: Standalone script
curl -fsSL https://get.pnpm.io/install.sh | sh -

# Verify installation
pnpm --version
pnpm store path

Essential pnpm Commands

pnpm commands closely mirror npm commands, making the transition straightforward. The key difference is that pnpm enforces stricter behavior by default and provides additional commands for monorepo management.

Installing Dependencies

The pnpm install command reads package.json and installs all dependencies. It creates or updates pnpm-lock.yaml and sets up the content-addressable node_modules structure.

# Install all dependencies from package.json
pnpm install

# Install with frozen lockfile (CI mode)
pnpm install --frozen-lockfile

# Install production dependencies only
pnpm install --prod

Adding and Removing Packages

Use pnpm add to add new packages and pnpm remove to uninstall them. pnpm add supports the same flags as npm install for dev, peer, and optional dependencies.

# Add a production dependency
pnpm add react react-dom

# Add a dev dependency
pnpm add -D typescript @types/react

# Add a peer dependency
pnpm add --save-peer react

# Add an optional dependency
pnpm add -O fsevents

# Add a specific version
pnpm add lodash@4.17.21

# Remove a package
pnpm remove lodash

# Remove from a specific workspace package
pnpm --filter @myorg/web remove lodash

Updating Packages

pnpm update checks for newer versions of installed packages. Use the --latest flag to ignore semver ranges and update to the absolute latest version.

# Update all packages within semver ranges
pnpm update

# Update to latest versions (ignore semver)
pnpm update --latest

# Update a specific package
pnpm update react

# Interactive update (select which to update)
pnpm update --interactive

# Update recursively in all workspaces
pnpm -r update

Running Scripts

pnpm run executes scripts defined in package.json. Unlike npm, pnpm does not automatically add node_modules/.bin to the PATH for lifecycle scripts, enforcing explicit script declarations.

# Run a script
pnpm run build
pnpm run dev
pnpm run test

# Shorthand (no "run" needed for common scripts)
pnpm build
pnpm dev
pnpm test

# Run in all workspace packages
pnpm -r run build

# Run in a specific workspace package
pnpm --filter @myorg/api run build

# Run a binary from node_modules/.bin
pnpm exec tsc --version
pnpm dlx create-next-app@latest  # like npx

Monorepo Workspaces with pnpm-workspace.yaml

pnpm has first-class monorepo support through its workspace feature. You define your workspace packages in a pnpm-workspace.yaml file at the repository root, and pnpm handles dependency linking, cross-package builds, and shared dependencies automatically.

The workspace protocol (workspace:*) lets you reference other packages in your monorepo. pnpm resolves these to the local versions during development and replaces them with actual version ranges when publishing. You can run commands across all workspaces or filter to specific packages using the --filter flag.

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

# With catalogs for shared versions
catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.7.0
  vitest: ^3.0.0
// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:^",
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vitest": "catalog:"
  }
}

// workspace:* -> resolves to local package during dev
// workspace:^ -> replaced with ^1.2.3 when publishing
// catalog:    -> resolves to version from pnpm-workspace.yaml
# Workspace filtering commands

# Run build in a specific package
pnpm --filter @myorg/web build

# Run build in package and all its dependencies
pnpm --filter @myorg/web... build

# Run tests in all packages that depend on @myorg/ui
pnpm --filter ...@myorg/ui test

# Run in all packages matching a pattern
pnpm --filter "@myorg/*" build

# Run in packages changed since main branch
pnpm --filter "...[origin/main]" build

# Add a dependency to a specific workspace
pnpm --filter @myorg/api add express

.npmrc Configuration

pnpm reads configuration from .npmrc files at the project, user, and global levels. Several pnpm-specific settings control how dependencies are resolved and how the store behaves. Place a .npmrc file in your project root to ensure consistent settings across your team.

# .npmrc - pnpm configuration

# Strict mode (recommended)
strict-peer-dependencies=true
auto-install-peers=true

# Do not hoist (default, keeps strict isolation)
shamefully-hoist=false

# Hoist specific packages that need flat access
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

# Store location (default: ~/.local/share/pnpm/store)
# store-dir=~/.pnpm-store

# Use lockfile v9 format
lockfile-format=v9

# Registry (default: https://registry.npmjs.org/)
# registry=https://registry.npmmirror.com/

# Node.js version for pnpm env
use-node-version=22.14.0

Strict Dependency Resolution

One of pnpm most important features is its strict dependency resolution. In the npm flat node_modules layout, any package in your dependency tree is importable from your application code, even if you did not explicitly declare it as a dependency. This leads to phantom dependencies: packages that work by accident because a transitive dependency happens to be hoisted.

pnpm solves this by creating a node_modules structure where only your direct dependencies are visible at the top level. Each package gets its own isolated set of dependencies through symlinks into the .pnpm directory. If you try to import a package not listed in your package.json, Node.js will throw a MODULE_NOT_FOUND error. This catches real bugs before they reach production.

// Example: Phantom dependency problem
// package.json only declares "express"
// But code imports "debug" (a transitive dep of express)

// With npm (works by accident):
const debug = require("debug"); // OK - hoisted to root

// With pnpm (fails correctly):
// Error: Cannot find module 'debug'
// MODULE_NOT_FOUND

// Fix: explicitly add the dependency
// pnpm add debug

// Now it works correctly with pnpm:
const debug = require("debug"); // OK - declared in package.json

Patching Third-Party Packages

Sometimes you need to fix a bug in a dependency without waiting for an upstream release. pnpm provides a built-in patching mechanism that lets you modify package source code and track the changes in your repository.

The pnpm patch command extracts the package to a temporary directory where you can make changes. After editing, pnpm patch-commit creates a diff file and adds a patchedDependencies entry to your package.json. The patch is automatically applied on every subsequent install.

# Step 1: Start patching a package
pnpm patch lodash@4.17.21
# Output: You can now edit the package at:
# /tmp/xxxx/node_modules/lodash

# Step 2: Make your changes in the temp directory
# Edit the files you need to fix

# Step 3: Commit the patch
pnpm patch-commit /tmp/xxxx/node_modules/lodash

# This creates patches/lodash@4.17.21.patch
# and adds to package.json:
# "pnpm": {
#   "patchedDependencies": {
#     "lodash@4.17.21": "patches/lodash@4.17.21.patch"
#   }
# }

# Remove a patch
pnpm patch-remove lodash@4.17.21

Overrides and Hooks

pnpm supports overrides for forcing specific versions of transitive dependencies. This is useful for security patches, deduplication, or fixing compatibility issues. Overrides are declared in the pnpm.overrides field of your root package.json.

For more advanced customization, pnpm supports a .pnpmfile.cjs hook file that lets you modify package metadata during resolution. You can use hooks to rewrite dependency versions, add missing peer dependencies, or transform package.json fields programmatically.

// package.json - overrides
{
  "pnpm": {
    "overrides": {
      "lodash": "^4.17.21",
      "got@<11.8.5": ">=11.8.5",
      "express>debug": "~4.3.0",
      "node-fetch": "npm:undici"
    }
  }
}
// .pnpmfile.cjs - hooks for advanced customization
function readPackage(pkg, context) {
  // Fix missing peer dependency
  if (pkg.name === "some-broken-package") {
    pkg.dependencies = {
      ...pkg.dependencies,
      "missing-peer": "^2.0.0"
    };
    context.log("Fixed missing peer dep for " + pkg.name);
  }

  // Force a specific version of a transitive dependency
  if (pkg.dependencies && pkg.dependencies["old-package"]) {
    pkg.dependencies["old-package"] = "^3.0.0";
  }

  return pkg;
}

module.exports = {
  hooks: {
    readPackage
  }
};

Understanding pnpm-lock.yaml

The pnpm-lock.yaml file records the exact versions, integrity hashes, and dependency relationships of every installed package. It is the source of truth for reproducible installations. Unlike package-lock.json, the YAML format is more readable and produces smaller diffs in version control.

The lockfile contains a packages section that maps package identifiers to their resolved versions and integrity checksums. It also records which dependencies each package requires, ensuring the entire dependency graph can be reconstructed deterministically.

# pnpm-lock.yaml structure (simplified)
lockfileVersion: "9.0"

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

importers:
  .:
    dependencies:
      react:
        specifier: ^19.0.0
        version: 19.0.0
    devDependencies:
      typescript:
        specifier: ^5.7.0
        version: 5.7.3

packages:
  react@19.0.0:
    resolution:
      integrity: sha512-abc123...
    engines:
      node: ">=18"

# Useful commands:
# pnpm install --frozen-lockfile  (CI - fail if lockfile outdated)
# pnpm install --lockfile-only    (update lockfile without installing)
# pnpm dedupe                     (reduce duplicate versions)

CI/CD Usage

pnpm is well-suited for CI/CD pipelines where fast, reproducible installations are critical. The pnpm install --frozen-lockfile flag (equivalent to npm ci) ensures the lockfile is not modified during install, failing the build if there are any mismatches. Combined with caching the pnpm store, CI builds can achieve near-instant dependency installation.

Most CI providers support pnpm natively or through simple setup steps. GitHub Actions has an official pnpm/action-setup action, and other platforms support Corepack activation.

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 10

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: "pnpm"

      - run: pnpm install --frozen-lockfile
      - run: pnpm run lint
      - run: pnpm run test
      - run: pnpm run build

Docker Integration

When using pnpm in Docker containers, you can leverage the content-addressable store for efficient layer caching. The recommended approach is to copy only the lockfile and workspace configuration first, install dependencies, then copy the source code. This ensures dependency layers are cached unless the lockfile changes.

# Dockerfile with pnpm
FROM node:22-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="\$PNPM_HOME:\$PATH"
RUN corepack enable

FROM base AS deps
WORKDIR /app
# Copy only files needed for install (better caching)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \\
    pnpm install --frozen-lockfile

FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run build

FROM base AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]

Migrating from npm or Yarn

Migrating to pnpm from npm or Yarn is straightforward. pnpm can import existing lockfiles from both package managers, preserving your resolved dependency versions. The main challenge is fixing phantom dependencies that your code may unknowingly rely on.

  • 1. Install pnpm and remove the old lockfile (package-lock.json or yarn.lock).
  • 2. Run pnpm import to convert the old lockfile to pnpm-lock.yaml (optional, for preserving resolutions).
  • 3. Run pnpm install to create the strict node_modules structure.
  • 4. Fix any MODULE_NOT_FOUND errors by explicitly adding missing dependencies to package.json.
  • 5. Update CI/CD scripts to use pnpm commands instead of npm or yarn.
  • 6. Add a packageManager field to package.json for Corepack.
# Migration from npm
corepack enable
pnpm import              # converts package-lock.json
rm package-lock.json
pnpm install             # creates node_modules + pnpm-lock.yaml

# Migration from yarn
corepack enable
pnpm import              # converts yarn.lock
rm yarn.lock
pnpm install

# Add packageManager field
# package.json:
# "packageManager": "pnpm@10.5.0"

# Update scripts in package.json
# "scripts": {
#   "preinstall": "npx only-allow pnpm"
# }

Performance Benchmarks

pnpm consistently outperforms npm in installation speed benchmarks, especially in scenarios with warm caches or shared stores. In a typical project with 500+ dependencies, pnpm install is 2-3x faster than npm install on a warm cache and 20-40% faster on a cold cache. The disk savings become more significant as the number of projects grows.

The content-addressable store means the second project using the same dependency version incurs zero download and near-zero disk cost. For organizations with many microservices or monorepos, this can save gigabytes of disk space and minutes of CI time per build.

# Benchmark: 500-dependency project (approximate times)
#
# Cold cache (first install):
#   npm install     ~45s
#   yarn install    ~38s
#   pnpm install    ~30s
#   bun install     ~12s
#
# Warm cache (store populated):
#   npm install     ~20s
#   yarn install    ~12s
#   pnpm install    ~8s
#   bun install     ~4s
#
# Disk usage (10 identical projects):
#   npm:  ~1.5 GB (10 copies)
#   pnpm: ~150 MB (1 copy + hard links)
#
# Run your own benchmark:
# hyperfine "pnpm install" "npm install" --prepare "rm -rf node_modules"

Side-by-Side Protocol and Catalogs

The workspace protocol (workspace:*) ensures local packages are always resolved from the workspace during development. When publishing, pnpm automatically replaces workspace: references with the actual version numbers. You can also use workspace:^ and workspace:~ for range-based references.

Catalogs are a pnpm workspace feature that lets you define shared dependency versions in a central location. Instead of duplicating version ranges across dozens of workspace packages, you declare a catalog of versions in pnpm-workspace.yaml and reference them with catalog: protocol. This ensures all packages in your monorepo use consistent dependency versions.

# pnpm-workspace.yaml with named catalogs
packages:
  - "packages/*"
  - "apps/*"

# Default catalog
catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.7.0

# Named catalogs for different groups
catalogs:
  testing:
    vitest: ^3.0.0
    "@testing-library/react": ^16.0.0
  linting:
    eslint: ^9.0.0
    prettier: ^3.4.0
// packages/ui/package.json
{
  "name": "@myorg/ui",
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vitest": "catalog:testing",
    "eslint": "catalog:linting"
  }
}

// Workspace protocol variants:
// "workspace:*"  -> exact local version
// "workspace:^"  -> ^major.minor.patch when published
// "workspace:~"  -> ~major.minor.patch when published

Best Practices

  • Always commit pnpm-lock.yaml to version control. It ensures reproducible builds and prevents dependency drift between environments.
  • Use Corepack and the packageManager field in package.json to pin your pnpm version. This guarantees every developer and CI environment uses the exact same version.
  • Set strict-peer-dependencies=true in .npmrc to catch peer dependency conflicts early during development instead of discovering them in production.
  • Use pnpm --filter to scope commands to specific workspace packages in monorepos, avoiding unnecessary work across the entire repository.
  • Cache the pnpm store directory in CI to dramatically speed up subsequent builds. The store path can be found with pnpm store path.
  • Run pnpm dedupe periodically to reduce duplicate package versions in your lockfile, shrinking node_modules and improving install times.
  • Use shamefully-hoist=false (the default) to maintain strict dependency isolation. Only enable hoisting when absolutely required for compatibility.
  • Prefer pnpm patch over forking dependencies for small fixes. Patches are tracked in version control and automatically applied on install.
  • Use catalogs in monorepos to centralize shared dependency versions, reducing maintenance burden and preventing version mismatches.
  • Run pnpm audit regularly and use overrides to force-upgrade vulnerable transitive dependencies.

Frequently Asked Questions

What is pnpm and how is it different from npm?

pnpm is a Node.js package manager that uses a content-addressable store with hard links instead of copying package files into each project. This saves significant disk space and speeds up installations. Unlike npm, pnpm creates a strict non-flat node_modules structure that prevents phantom dependencies, meaning your code cannot accidentally import packages not listed in your package.json.

How does pnpm save disk space?

pnpm stores every package file in a global content-addressable store, identified by its content hash. When a project needs a package, pnpm creates hard links from node_modules to the store instead of copying files. If 10 projects use the same version of React, the files exist only once on disk. Different versions of a package share files that have identical content, further reducing storage.

Can I use pnpm with existing npm projects?

Yes. pnpm is fully compatible with the npm registry and npm package.json format. You can migrate by running pnpm import to convert your package-lock.json, then pnpm install to create the new node_modules structure. The main adjustment is fixing any phantom dependency imports that worked with npm flat hoisting but fail under pnpm strict resolution.

What are phantom dependencies and why does pnpm prevent them?

Phantom dependencies are packages that your code imports but are not listed in your package.json. They work in npm because npm hoists all transitive dependencies to a flat node_modules root. If a transitive dependency is removed or updated by another package, your code breaks unexpectedly. pnpm prevents this by only exposing packages you explicitly declare, catching these issues during development.

How do pnpm workspaces compare to npm workspaces?

pnpm workspaces provide more features than npm workspaces, including the workspace: protocol for local package references, the --filter flag for targeting specific packages, catalogs for shared dependency versions, and the ability to run commands recursively with pnpm -r. pnpm also handles workspace dependency linking more efficiently through its symlink-based node_modules structure.

How do I set up pnpm with Corepack?

Enable Corepack with corepack enable, then add a packageManager field to your package.json specifying the exact pnpm version, such as pnpm@10.5.0. When anyone runs pnpm in the project directory, Corepack automatically downloads and uses that exact version. This ensures consistent tooling across your entire team without manual installation steps.

Is pnpm compatible with all npm packages?

pnpm is compatible with the vast majority of npm packages. Rare compatibility issues arise when packages rely on the flat node_modules layout to access undeclared dependencies. In such cases, you can use the shamefully-hoist or public-hoist-pattern settings in .npmrc to selectively hoist problematic packages. Most modern packages work correctly with pnpm out of the box.

How fast is pnpm compared to npm and Yarn?

In benchmarks, pnpm install is typically 2-3x faster than npm install with a warm cache and 20-40% faster on a cold cache. Compared to Yarn with node_modules, pnpm is comparable in speed. Yarn PnP can be faster for initial installs since it avoids creating node_modules entirely. Bun is the fastest overall due to its native runtime, but pnpm offers the best balance of speed, strictness, and disk efficiency.

𝕏 Twitterin LinkedIn
Was this helpful?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Try These Related Tools

{ }JSON FormatterY→YAML to JSON Converter

Related Articles

Turborepo Complete Guide: High-Performance Monorepo Build System (2026)

Comprehensive Turborepo guide covering turbo.json configuration, pipelines, remote caching, workspace setup, shared packages, TypeScript, CI/CD, Docker pruning, generators, and migration.

Bun: The Complete Guide to the All-in-One JavaScript Runtime

Master Bun runtime with package manager, bundler, test runner, HTTP server, SQLite, shell scripting, and Node.js/Deno comparison.

Deno: The Complete Guide to the Secure JavaScript Runtime

Master Deno runtime with security permissions, TypeScript support, standard library, HTTP server, testing, npm compatibility, and Deno Deploy.