---
title: "How to migrate from npm to pnpm"
slug: "migrate-from-npm-to-pnpm"
category: "howto"
tags: ["howto", "pnpm", "npm", "package-manager", "monorepo", "migration", "tooling"]
status: "stable"
last_updated: 2026-08-14
summary: "Replace npm with pnpm in a project or monorepo: install pnpm, import the lock file, fix phantom dependencies, and update CI."
related: ["[[comparisons/yarn-vs-pnpm]]", "[[howto/set-up-bun-as-a-package-manager]]", "[[comparisons/turborepo-vs-nx]]", "[[tooling/github]]", "[[coding/general-principles]]", "[[howto/set-up-github-actions-cache]]", "[[comparisons/vite-vs-webpack]]"]
---

## Overview

Migrating from npm to pnpm saves disk space, enforces strict dependency isolation, and speeds up installs. The migration is low-risk: pnpm reads `package.json` and the npm registry unchanged. The main work is fixing phantom dependencies that npm's hoisting hid, and updating CI scripts. See [[comparisons/yarn-vs-pnpm]] for when to use pnpm versus Yarn.

## Prerequisites

- Node 22 on the path.
- A project using `package-lock.json`. Yarn migrations are similar but start from `yarn.lock`.
- CI access to update install steps. For GitHub Actions, see [[howto/set-up-github-actions-cache]].
- For monorepos: a list of all workspace packages.

## Steps

### 1. Install pnpm

```bash
npm install -g pnpm
pnpm --version
```

Or use Corepack, which ships with Node 22:

```bash
corepack enable
corepack prepare pnpm@latest --activate
```

### 2. Import the lock file

`pnpm import` reads `package-lock.json` and generates a `pnpm-lock.yaml` with equivalent resolved versions. This preserves the exact versions npm resolved, avoiding unexpected upgrades.

```bash
pnpm import
```

Commit `pnpm-lock.yaml` before continuing. Do not delete `package-lock.json` yet.

### 3. Install with pnpm and surface phantom dependency errors

```bash
rm -rf node_modules
pnpm install
```

pnpm's strict linker does not hoist all packages. If the project uses a package that is a transitive dependency but not declared in `package.json`, the build will fail with a resolution error.

Fix each phantom dependency by adding it explicitly:

```bash
pnpm add package-name
```

These are real bugs npm's hoisting was hiding. Every package you add is a dependency the project actually needs.

### 4. Update scripts in `package.json`

Replace `npm run` calls inside scripts with `pnpm run`. Most scripts work unchanged; the main difference is lifecycle hooks.

```json
{
  "scripts": {
    "build": "vite build",
    "test": "vitest run",
    "postinstall": "pnpm run generate"
  }
}
```

### 5. Configure monorepo workspaces (if applicable)

npm workspaces use a `workspaces` field in the root `package.json`. pnpm uses a separate `pnpm-workspace.yaml` file.

Create `pnpm-workspace.yaml` at the root:

```yaml
packages:
  - "apps/*"
  - "packages/*"
```

Remove the `workspaces` field from the root `package.json` (pnpm ignores it in favor of the YAML file).

### 6. Update CI

Replace `npm ci` with `pnpm install --frozen-lockfile`. For GitHub Actions:

```yaml
- 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 build
- run: pnpm test
```

Pin a major version explicitly rather than omitting `version` or using `latest`; pnpm 9 reached end-of-life in April 2026, so a pin that was correct a year ago can now install an unsupported release.

The `cache: pnpm` option in `actions/setup-node` caches the pnpm store between runs. See [[howto/set-up-github-actions-cache]] for caching strategy details.

### 7. Delete the npm lock file

After CI passes with pnpm on a test branch, delete `package-lock.json` and commit.

```bash
rm package-lock.json
git add pnpm-lock.yaml pnpm-workspace.yaml
git rm package-lock.json
git commit -m "feat: migrate from npm to pnpm"
```

## Verify it worked

```bash
# Clean install from lock file
rm -rf node_modules
pnpm install --frozen-lockfile

# Build and test
pnpm run build
pnpm test
```

Both commands should exit 0. If they do, the migration is complete.

## Common errors

- **`ERR_PNPM_MISMATCHED_LOCKFILE`**: `pnpm-lock.yaml` is out of sync with `package.json`. Run `pnpm install` to regenerate, then commit.
- **Package not found at import time**: a phantom dependency. Add it explicitly with `pnpm add package-name`.
- **Postinstall script failing**: some packages run `npm` in postinstall scripts. Set `pnpm.overridePackageManager = false` in `package.json` or patch the script.
- **Monorepo cross-package imports fail**: ensure `pnpm-workspace.yaml` lists all package globs correctly. Run `pnpm ls -r` to verify all packages are discovered.
- **CI cache miss every run**: confirm the `cache: pnpm` setup in `actions/setup-node` uses `pnpm-lock.yaml` as the cache key. The default behavior does this automatically.

## Related

- [[comparisons/yarn-vs-pnpm]]
- [[howto/set-up-bun-as-a-package-manager]]
- [[comparisons/turborepo-vs-nx]]
- [[tooling/github]]
- [[coding/general-principles]]
- [[howto/set-up-github-actions-cache]]
