Home / Articles

Switching a Node project from npm to pnpm without breaking the team

npm vs pnpm on disk layout, phantom dependencies and lockfiles, then a Node.js team migration with pnpm import, only-allow and a pinned packageManager field.

Publisher: REACTAPP.TOP6 min read1317 words

This article is published in English.

Sources: Switching from npm to pnpm

Package managers decide how dependencies land on disk, which lockfile you commit, and how strict imports are when something goes wrong. For many Node projects the default is still npm. Teams that maintain several repositories also live with a steady stream of security advisories that ask for version bumps. Dependabot can open some of those updates automatically; other upgrades need manual work because transitive fixes collide, or because the lasting answer is to replace a package entirely.

Around the same period GitHub adjusted Dependabot so non-security version PRs wait three days before opening. That policy change is mostly orthogonal to package-manager choice, but it signals how noisy automatic upgrades had become.

Meanwhile supply-chain incidents keep showing up in headlines. Compromised or vulnerable packages sit inside apps that pull in icon sets, auth helpers, and token utilities alike, so the blast radius grows with every transitive edge. Security alerts rarely arrive one at a time. A laptop that clones a handful of services will see advisories for transitive parsers, UI kits, and auth middlewares in the same week. Automatic bots help when the bump is mechanical; they struggle when a major version rewrites APIs or when two dependencies disagree on a shared peer. In those weeks it is reasonable to ask whether the installer itself can reduce risk—by refusing phantom dependencies, by sharing content on disk so audits see one copy of a version, and by slowing down brand-new publishes until the ecosystem has had a look.

One recurring suggestion is to leave npm for pnpm: similar day-to-day commands, a stricter install model, and usually less disk thrash. The sections below compare the two without claiming a universal winner—only enough detail to decide whether a switch is worth the migration cost.

General info on npm / pnpm

A short baseline for each tool:

npm

  • Ships as the default package manager with Node.js.
  • Owns dependency installation and package.json scripts.
  • Everyday commands include npm install (install from the lock/manifest), npm install <name> (add a dependency), npm run <script>, and npm uninstall <name>.
  • Writes package-lock.json.
  • Materializes packages under node_modules.

npm also ships npx, which runs a package without adding it to the project or installing it globally. Scaffolding tools such as npx create-vite@latest project-name download temporarily, execute, and leave package.json alone. The distinction from npm install is intentional: one-shot execution versus a lasting dependency.

pnpm

  • A third-party package manager with the same job—dependencies and scripts—implemented differently on disk.
  • Familiar verbs map closely: pnpm install, pnpm add <name>, pnpm run <script>, pnpm remove <name>.
  • Typically uses less disk space and finishes installs faster by storing each version once.
  • Restricts imports to what package.json declares.
  • Uses pnpm-lock.yaml instead of package-lock.json.
  • Applies stricter defaults around newly published versions (for example delaying uptake).

The one-shot runner analogous to npx is pnpm dlx—useful when scaffolding with create-vite at latest, for instance. Many generators also accept pnpm create. For binaries already present in node_modules, pnpm exec is the local runner.

Pros and cons of pnpm

Trade-offs matter as much as slogans:

  • npm copies packages into each project’s node_modules, which duplicates content across repos; pnpm keeps one content-addressable copy per version and links from projects.
  • That layout usually shrinks disk use and speeds repeated installs.
  • npm’s historically flatter resolution can let code require a package that was never declared; pnpm refuses undeclared imports, which shrinks accidental attack surface.
  • pnpm’s delayed adoption of brand-new releases reduces the chance of immediately pulling a compromised publish.
  • npm has gained related capabilities over time, but several remain opt-in rather than default.
  • pnpm needs a separate install step; npm arrives with Node.
  • Older plugins that assume npm’s layout may need tweaks under pnpm’s stricter linking.
  • Team muscle memory still skews toward npm, so onboarding can take a short adjustment period.

Disk layout differences show up quickly on CI runners that cache node_modules poorly and on developer machines that keep a dozen apps checked out. Content-addressable storage means the second and third projects that need lodash@4.x mostly create links instead of another full tree. Stricter dependency visibility also changes debugging: when an import fails because the package was never declared, the failure is loud at install or typecheck time instead of a surprising runtime require that only worked because another package happened to pull it in.

Switching to pnpm

A minimal global install looks like:

// Installation command (once)
npm install -g pnpm

// Usage
pnpm install
pnpm add [package name]
pnpm run [script name]
pnpm remove [package name]

After the first successful install in a project, expect pnpm-lock.yaml beside (and eventually instead of) package-lock.json.

Other install methods exist, including standalone scripts; the pnpm installation docs list them.

Migrating an existing npm project usually means deleting node_modules and package-lock.json, then running pnpm install. Most apps come up cleanly; awkward native addons or legacy resolvers may need one-off fixes.

To seed versions from an existing lockfile, run pnpm import. To clear npm leftovers:

// For macOS/Linux/Git Bash:
rm -rf node_modules package-lock.json

// For Windows PowerShell:
rm -Recurse -Force node_modules, package-lock.json

To block accidental npm install later, add a preinstall guard:

"scripts": {
  "preinstall": "npx only-allow pnpm"
}

Then add the helper as a dev dependency:

pnpm add -D only-allow

Pinning the exact CLI for collaborators uses the packageManager field (swap the version string as needed):

{
  "packageManager": "pnpm@11.22.0"
}

That pin keeps CI and laptops on one CLI release; remember to bump it deliberately when the team upgrades.

Inspect and self-update with:

pnpm --version

pnpm self-update

If self-update misbehaves, reinstall globally with npm install -g pnpm and restart the shell.

After a successful switch you should see:

  • package-lock.json gone.
  • pnpm-lock.yaml present.
  • package.json unchanged aside from any pins or scripts you added.
  • node_modules rewritten under pnpm’s linker (or recreated by a fresh pnpm install).
  • Day-to-day commands that mostly differ by the leading tool name.
  • One-shot runners via pnpm dlx where you previously used npx.

Official documentation remains the place to chase edge cases; the notes above are enough to evaluate the migration and perform a routine cutover.

Treat the first migrated repository as a rehearsal: run the test suite, confirm Docker builds still resolve, and check that any native addons compile under the new linker. Once that path is boring, spreading the same packageManager pin and only-allow guard across sibling services keeps the fleet consistent. If a legacy tool truly cannot tolerate pnpm’s layout, isolate it rather than abandoning the stricter defaults everywhere else.