VD
architecture2025-12-0511 min

npm, pnpm, and Yarn: A Practical Guide to JavaScript Package Managers

A detailed, practical look at npm, pnpm, and Yarn — what they do, how they differ, and how to choose the right package manager for your next JavaScript or TypeScript project.

npm, pnpm, and Yarn: A Practical Guide to JavaScript Package Managers

Modern JavaScript development is unthinkable without package managers. React, TypeScript, ESLint, Webpack, Vite, Jest, Cypress — almost everything you use comes from npm packages.

But there isn’t just one way to manage them.

Today, the three most common tools are:

  • npm — the default, ships with Node.js
  • Yarn — introduced faster installs and better workflows
  • pnpm — focuses on speed and disk efficiency with a clever storage model

In this post, we’ll walk through:

  • What a package manager actually does
  • The core concepts that npm, pnpm, and Yarn share
  • How each tool behaves, with concrete examples
  • Strengths and trade-offs of each
  • How to pick one for your projects and monorepos

1. What Does a Package Manager Do?

At a high level, a package manager:

  • Installs dependencies listed in your package.json
  • Resolves and pins exact versions in a lockfile
  • Makes installed packages available to your application, usually through node_modules or Yarn Plug'n'Play
  • Runs scripts defined in package.json (lint, test, build, etc.)
  • Publishes packages to registries like npmjs.com

A minimal package.json might look like:

{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest"
  },
  "dependencies": {
    "react": "^18.3.0",
    "react-dom": "^18.3.0"
  },
  "devDependencies": {
    "vite": "^5.0.0",
    "vitest": "^2.0.0"
  }
}

All three package managers understand this file. The differences lie in:

  • Install speed
  • Disk space usage
  • Lockfile format
  • Workspace/monorepo support
  • Extra features and workflows

2. Core Concepts: Same Ideas, Different Implementations

Before we dive into each tool, let’s align on concepts they all share.

2.1 Dependencies and Version Ranges

In package.json:

"dependencies": {
  "axios": "^1.7.0",
  "zustand": "~5.0.0"
}

Common version range operators:

  • ^1.7.0

    • Accepts >=1.7.0 <2.0.0
    • Allows new features and bug fixes, but blocks breaking changes
  • ~5.0.0

    • Accepts >=5.0.0 <5.1.0
    • Only patch updates; more conservative

An exact version:

"typescript": "5.6.3"

This pins one release. A lockfile records the resolved dependency graph, but reproducible CI also depends on using an install mode that refuses to rewrite that file.

2.2 Lockfiles

Each tool has its own lockfile format:

  • npm: package-lock.json
  • Yarn Classic and modern Yarn: yarn.lock (with different formats)
  • pnpm: pnpm-lock.yaml

Lockfiles store exact versions and integrity hashes so that:

  • Local development, CI pipelines, and production builds can use the same dependency graph
  • The package manager can reuse work already captured during dependency resolution

Rule of thumb:

Commit your lockfile to Git for apps. For libraries, lockfile strategy can vary, but you almost always commit it for the primary repo.

In CI, use the immutable install command for your package manager:

# npm: requires package-lock.json and never rewrites it
npm ci

# Modern Yarn: fails if yarn.lock would change
yarn install --immutable

# pnpm: fails if pnpm-lock.yaml is missing or out of sync
pnpm install --frozen-lockfile

These commands turn a lockfile mismatch into a failed build instead of silently changing the dependency graph. The official documentation describes the exact behavior of npm ci (opens in a new tab), yarn install --immutable (opens in a new tab), and pnpm install --frozen-lockfile (opens in a new tab). Modern Yarn and pnpm enable similar immutable behavior automatically in many CI environments, but keeping the flags explicit makes the intent clear.

2.3 Scripts

All of them support npm run style scripts:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "test": "vitest run",
  "lint": "eslint src --ext .ts,.tsx"
}

Commands:

  • npm: npm run dev
  • pnpm: pnpm dev (shorthand for pnpm run dev)
  • Yarn: yarn dev

All three make binaries from declared dependencies available while running scripts, so you can use tools without global installs. npm, pnpm, and Yarn's node_modules linker use a .bin directory; Yarn Plug'n'Play exposes binaries without creating node_modules/.bin.


3. npm: The Default Package Manager

3.1 What Is npm?

npm is:

  • The default package manager installed with Node.js
  • The name of the client (npm CLI)
  • Also the name of the registry (https://registry.npmjs.org)

If you install Node, you get npm automatically:

node -v
npm -v

3.2 Basic npm Workflow

Initialize a project:

npm init -y

Install dependencies:

# Add a regular dependency
npm install react react-dom

# Add a dev dependency
npm install --save-dev typescript vite

This will:

  • Update dependencies or devDependencies in package.json
  • Create or update package-lock.json
  • Populate node_modules/ with installed packages

Run scripts:

npm run dev
npm run build
npm test

Use npx (or npm exec) for one-off commands:

npx create-next-app my-app
npx vitest

3.3 Pros of npm

  • Comes with Node; zero extra setup
  • The most widely assumed baseline across the Node.js ecosystem
  • Actively improved; performance and features have gotten much better in recent versions
  • npm workspaces are now a built-in way to manage monorepos

Example of npm workspaces:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

Then:

npm install
npm run build --workspace apps/web

3.4 Cons / Limitations

Compared with alternatives, npm's main trade-off is its conventional, hoisted node_modules layout. It is broadly compatible, but repeated projects can consume more disk space than pnpm's shared content-addressable store or Yarn Plug'n'Play.

Install performance is workload-dependent. Recent npm versions can be competitive, so benchmark your actual repository before switching package managers for speed alone.


4. Yarn: Classic Workflows and Modern Plug'n'Play

4.1 Why Yarn Was Created

Yarn was originally created by Facebook (and others) to address issues they saw with npm at the time:

  • Slow and sometimes unreliable installs
  • Non-deterministic dependency resolution
  • Problems working on very large codebases

Yarn introduced:

  • A different lockfile format (yarn.lock)
  • Parallelized downloads and caching for speed
  • A focus on deterministic installs

4.2 Yarn Classic vs Modern Yarn

There are two “families” of Yarn:

  • Yarn Classic (v1)

    • Very popular, especially in older projects
    • Uses node_modules like npm
    • Great support for workspaces
  • Modern Yarn (v2+), sometimes called Berry

    • Completely redesigned
    • Uses Plug'n'Play (PnP) by default for new projects
    • Can instead use a traditional node_modules linker
    • Adds constraints, plugins, zero-installs, and stricter dependency checks

They share familiar commands, but they are not interchangeable. Check a repository's packageManager field and Yarn configuration before copying commands between Classic and modern projects.

4.3 Basic Yarn Workflow

Initialize a modern Yarn project:

yarn init

Yarn Classic also supports yarn init -y for accepting defaults. Modern Yarn's yarn init documentation (opens in a new tab) uses flags such as -p for a private package and -w for a workspace root instead.

Install dependencies:

# Add a regular dependency
yarn add react react-dom

# Add a dev dependency
yarn add --dev typescript vite

Run scripts:

yarn dev
yarn build
yarn test

Install all dependencies (after cloning a repo):

yarn install

4.4 Yarn Workspaces

Yarn popularized the concept of workspaces for monorepos.

In your root package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

Now:

  • apps/web can depend on @my-scope/ui from packages/ui
  • Yarn links them locally (no publishing required)
  • yarn install deduplicates dependencies across all workspaces

Workspaces are extremely useful for:

  • Shared components (packages/ui)
  • Utility libraries (packages/utils)
  • Config packages (packages/eslint-config, packages/tsconfig)

4.5 Plug'n'Play in Modern Yarn

Plug'n'Play changes how modules are located:

  • No node_modules folder
  • Yarn generates a .pnp.cjs loader containing the dependency map
  • Cached packages are commonly stored as one zip archive per package
  • The loader resolves imports directly to those package locations instead of crawling node_modules

Pros:

  • Less filesystem work during installation
  • Strong protection against undeclared, or “ghost,” dependencies
  • A dependency cache that can be shared across projects or committed for zero-installs

Cons:

  • Some tools and IDE integrations need PnP-aware configuration
  • React Native and Expo require a node_modules linker
  • Slightly steeper learning curve

Modern Yarn can opt back into a conventional layout with nodeLinker: node-modules in .yarnrc.yml. Yarn's Plug'n'Play guide (opens in a new tab) explains both the loader model and the compatibility trade-offs.


5. pnpm: Performance and Disk Efficiency

5.1 What Makes pnpm Different?

pnpm was created to solve two big problems:

  1. Repeated copies of the same package files consuming disk space
  2. Repeated work when the same packages are installed across projects

Its core idea:

Use a content-addressable store, hard links, and symlinks instead of copying every package into every project.

In plain English:

  • pnpm keeps package content in a shared store
  • Package files are hard-linked or reflinked into each project's virtual store
  • Symlinks then construct the dependency graph exposed through node_modules
  • Projects using identical package content can reuse the same files on disk

5.2 Basic pnpm Workflow

Install pnpm (one-time):

npm install -g pnpm

Initialize:

pnpm init

Install dependencies:

# Regular dependency
pnpm add react react-dom

# Dev dependency
pnpm add -D typescript vite

Run scripts:

pnpm dev
pnpm build
pnpm test

Install all dependencies:

pnpm install

5.3 pnpm’s Node Modules Layout

pnpm creates a special node_modules structure:

  • Package files appear under a virtual store, normally node_modules/.pnpm
  • Those files are linked from pnpm's content-addressable store
  • Symlinks connect dependencies to one another and expose direct dependencies at the project root
  • The layout remains compatible with Node's module resolution algorithm

Benefits:

  • Massive disk space savings, especially in monorepos
  • Faster installs once packages are cached
  • Helps catch bad patterns like accessing undeclared dependencies

The exact global store location is platform- and configuration-dependent; use pnpm store path rather than assuming a path such as ~/.pnpm-store. The pnpm layout documentation (opens in a new tab) walks through the hard links and symlinks step by step.

5.4 pnpm Workspaces

pnpm has first-class support for workspaces via pnpm-workspace.yaml:

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

Then:

pnpm install
pnpm -r test      # run test in all packages (recursive)
pnpm -r build

The -r (recursive) flag is extremely handy in monorepos.

5.5 Why Many Monorepos Prefer pnpm

You’ll often see pnpm used in larger monorepos because:

  • It uses far less disk space
  • Workspace support is solid and simple
  • It enforces better dependency hygiene
  • It keeps install times very competitive

For a big repo with many packages and branches, these benefits add up quickly.


6. Comparing npm, pnpm, and Yarn

Here’s a simplified overview:

Feature / AspectnpmYarnpnpm
Default with Node.jsYesNoNo
Lockfilepackage-lock.jsonyarn.lockpnpm-lock.yaml
Default install modelHoisted node_modulesClassic: node_modules; modern: PnPLinked node_modules with a virtual store
Cross-project disk reuseDownload cache; project files are installed separatelyStrong with PnP and its cacheStrong through the content-addressable store
WorkspacesBuilt inBuilt inBuilt in
Immutable CI installnpm ciyarn install --immutablepnpm install --frozen-lockfile
No node_modules optionNoYes, with modern Yarn PnPNo
Adoption effortUsually lowestHigher when adopting modern Yarn or PnPModerate; tooling must tolerate the linked layout

Cold and warm install speed can change with the dependency graph, cache state, filesystem, network, lifecycle scripts, and package-manager version. Treat published benchmarks as starting points, then measure the commands your repository actually runs in local development and CI.

All three are good choices in 2026. The question is more about fit than “which one is objectively best.”


7. Choosing the Right Package Manager

Here are some practical guidelines.

7.1 When npm Is a Great Choice

Use npm if:

  • You prefer simplicity and minimal tooling decisions
  • You’re working on small to medium apps
  • Your team is new to Node/JS tooling
  • You want to avoid explaining extra global tools

Recent npm versions support:

  • Workspaces
  • Lockfiles
  • Good performance

For many apps, npm is more than enough.

7.2 When Yarn Makes Sense

Use Yarn if:

  • Your team is familiar with Yarn from past projects
  • You want workspaces, and your organization already standardized on Yarn
  • You want modern Yarn's constraints, plugins, zero-installs, or PnP dependency checks
  • You have verified that your framework, IDE, and deployment tooling support your chosen linker

For an existing Yarn repository, staying on its current major version may be less risky than migrating package managers and upgrading Yarn at the same time.

7.3 When pnpm Shines

Use pnpm if:

  • You’re building a monorepo with many packages
  • Disk usage and install performance matter a lot (e.g., CI, many branches)
  • You want stricter dependency resolution to catch bad imports
  • You like the idea of a shared, content-addressable store

pnpm is especially attractive for:

  • Design system repos (packages/ui, packages/tokens, etc.)
  • Backend + frontend apps sharing common utilities
  • Multi-app setups with shared libraries

8. Migrating Between Package Managers

You can switch package managers, but a migration changes more than the command developers type. Different resolution and linking models can expose peer-dependency conflicts, undeclared dependencies, or tool assumptions.

Use a migration sequence that preserves a rollback point:

  1. Start from a clean branch with the current lockfile committed
  2. Pin the target tool and version in the root package.json
  3. Import the existing lockfile when the target package manager supports it
  4. Remove or move generated install artifacts such as node_modules so the next install uses the target tool's layout
  5. Install dependencies and inspect the new lockfile
  6. Run tests, builds, local development, and the immutable CI install
  7. Remove the old lockfile and obsolete tool-specific configuration only after validation

For example, pnpm can generate pnpm-lock.yaml from package-lock.json or yarn.lock:

pnpm import
pnpm install --frozen-lockfile
pnpm test
pnpm build

The pnpm import command (opens in a new tab) preserves more of the existing resolution intent than deleting the old lockfile and resolving everything from scratch. Conversion is not a guarantee of an identical graph, so review the diff and test before removing the source lockfile.

After migrating:

  • Pick one per repo
  • Pin it with a packageManager field such as "packageManager": "pnpm@<version>"
  • Document it in your README
  • Update contributor setup, CI caches, deployment configuration, and dependency automation
  • Avoid mixing package managers in the same working tree

9. Summary

npm, pnpm, and Yarn all solve the same core problem: managing JavaScript dependencies. But they do it with different trade-offs:

  • npm: simple, stable, and ships with Node. Great default for many apps.
  • Yarn: established deterministic workflows and workspaces, while modern Yarn adds PnP, constraints, plugins, and zero-installs.
  • pnpm: aggressively efficient with disk and installs, ideal for monorepos and large codebases.

If you’re starting a new project today:

  • For a single app or small project: npm or pnpm
  • For a monorepo with multiple apps and packages: pnpm or Yarn workspaces
  • For teams already standardized on something: stick with what your team knows, unless you have a strong reason to switch

Ultimately, the “best” package manager is the one your team understands and uses consistently, backed by good practices:

  • Committed lockfiles
  • A pinned package-manager version
  • Immutable installs in CI
  • Clear scripts
  • Consistent workflow in local dev and CI

Pick one, learn it well, and let it disappear into the background while you focus on building great products.

VD

Vikram Dokkupalle

Frontend Engineer & UI/UX Enthusiast. Passionate about React, performance, and clean design.

More from architecture

View all posts
2026-07-2320 min

Designing Reliable Multi-Agent Systems

Learn when to use prompts, skills, tools, one agent, or multiple agents—and how to keep agent systems understandable, safe, and reliable.

2026-07-2016 min

Tokenomics for Developer Teams: Managing Monthly AI Budgets Without Anxiety

How engineering teams can manage monthly AI allowances, optimize developer usage, and prevent budget limits from turning into token anxiety.

2026-07-1919 min

Accessibility from Day One: Building It into Components, Teams, and CI

A practical guide to treating accessibility as an engineering practice—from semantic components and keyboard behavior to team habits, automated testing, and CI guardrails.