| |

Node.js 7 🟢 Package Management with npm, pnpm, and yarn

Node.js has three major package managers: npm, pnpm, and yarn. All three install packages from the npm registry, manage versions through a lockfile, and support workspaces for monorepos. The differences lie in how they store packages on disk, how fast they install, and how strictly they enforce dependency boundaries. npm is the default and ships with Node.js, pnpm is the fastest and most disk-efficient, and yarn sits between them with a mature ecosystem and a unique Plug’n’Play mode that eliminates node_modules entirely.

Choosing a package manager is not a one-time decision. It determines how dependencies are laid out, how quickly CI pipelines run, and whether code can accidentally import packages it never declared. Understanding the storage strategies—flat trees, hard links, and content-addressable stores—explains why pnpm installs are faster and why its node_modules directory is smaller. Understanding lockfiles explains how reproducible builds work across environments.

This chapter covers npm’s flat dependency tree and lockfile, pnpm’s content-addressable store and symlink structure, yarn’s classic and modern modes, the trade-offs between them, and the patterns for choosing one for a project.

Key point: npm uses a flat node_modules with hoisting and package-lock.json. pnpm uses a content-addressable store with hard links and a non-flat node_modules, which saves disk space and prevents undeclared imports. Yarn offers both a classic mode similar to npm and a modern Plug’n’Play mode that eliminates node_modules. All three use lockfiles for reproducible installs.


Why three package managers exist

The evolution problem. npm was the first package manager for Node.js and remains the default. Early versions installed dependencies in a nested tree, which caused deep directory structures and duplicate packages. npm 3 introduced flat hoisting to reduce duplication, but hoisting introduced a new problem: code could import packages it never declared, because transitive dependencies were hoisted to the top level . Yarn was created by Facebook in 2016 to address speed and determinism issues in npm. pnpm was created to address disk space and dependency isolation.

The disk space problem. When you have multiple projects that use the same dependency, npm and yarn classic copy that dependency into each project’s node_modules. Ten projects using lodash means ten copies of lodash on disk. pnpm stores every version of every package exactly once in a global content-addressable store and hard-links files into each project’s node_modules . The disk savings compound as the number of projects grows.

The phantom dependency problem. npm’s flat hoisting makes it possible to import a package that is not in your package.json, because a transitive dependency was hoisted to the root of node_modules. This is called a phantom dependency, and it breaks when the transitive dependency is removed or updated. pnpm’s non-flat node_modules structure prevents this: only declared dependencies are linked into the project, so code cannot import what it did not declare .

The speed problem. Installation speed depends on three factors: how much is downloaded, how much is copied or linked, and how much is done in parallel. pnpm resolves, fetches, and links in parallel, and on a warm store it mostly creates links rather than copying files . This makes pnpm installs significantly faster than npm on repeat installs, especially in CI where the store can be cached.

The lockfile problem. All three package managers use a lockfile to record the exact versions of every dependency in the tree. npm uses package-lock.json, pnpm uses pnpm-lock.yaml, and yarn uses yarn.lock. Committing the lockfile ensures that every developer and every CI run installs the same versions, eliminating “works on my machine” dependency drift .


a. npm: the default package manager

npm ships with Node.js and is the most widely used package manager. Its storage strategy is a flat node_modules tree with hoisting.

Initialization creates a package.json:

npm init -y

Installing dependencies writes them to package.json and installs them into node_modules:

npm install express
npm install --save-dev typescript

The --save-dev flag records the package under devDependencies rather than dependencies.

Removing:

npm uninstall express
npm uninstall --save-dev typescript

Reproducible installs use npm ci, which reads package-lock.json and installs exactly what is recorded, failing if package.json and the lockfile are out of sync. This is the recommended command for CI .

npm ci

Auditing checks for known vulnerabilities:

npm audit
npm audit fix

The flat tree and hoisting. npm 3 and later flatten the dependency tree by hoisting dependencies to the top level of node_modules whenever there is no version conflict. This reduces duplication and shortens paths, but it also allows undeclared imports. The package-lock.json file records the exact resolved tree, so the hoisting is deterministic across installs .

Global installs place packages in a system directory and expose their binaries on the PATH:

npm install -g pnpm

Global installs are convenient for CLI tools but should not be used for project dependencies, because they are not recorded in package.json and are not reproducible.


b. pnpm: content-addressable storage and hard links

pnpm (performant npm) stores every file from every package version exactly once in a global content-addressable store. When a project installs a package, pnpm hard-links the files from the store into the project’s node_modules. No copying occurs, and the same version of a package shared across projects consumes disk space only once .

Installation is typically via npm:

npm install -g pnpm

Adding dependencies:

pnpm add express
pnpm add -D typescript

Installing from lockfile:

pnpm install --frozen-lockfile

The --frozen-lockfile flag is the CI equivalent of npm ci: it fails if the lockfile would need to change.

The non-flat node_modules structure. pnpm does not hoist dependencies. Instead, it creates a nested structure where each package’s dependencies are symlinked into place. The root of node_modules contains only the project’s declared dependencies, which means code cannot import a package that is not in package.json . This eliminates phantom dependencies.

The store location can be inspected:

pnpm store path

Workspace support. pnpm has first-class support for monorepos through pnpm-workspace.yaml. Packages within the workspace can reference each other with the workspace: protocol, and a single lockfile covers all packages .

Security features. pnpm does not run install scripts for arbitrary dependencies by default. Build scripts for packages must be explicitly approved, which reduces the attack surface from malicious packages .

Patching dependencies. pnpm supports creating persistent patches for dependencies without waiting for upstream fixes, using pnpm patch and pnpm patch-commit .


c. yarn: classic and modern modes

Yarn was created to improve on npm’s speed and determinism. It has two modes: Classic (v1) and Modern (v2+).

Classic yarn (v1) uses a node_modules directory similar to npm and a yarn.lock file for determinism. It introduced parallel downloads and a global cache that made repeat installs faster than npm at the time.

yarn add express
yarn add --dev typescript
yarn remove express
yarn install --frozen-lockfile

Modern yarn (v2+) introduced Plug’n’Play (PnP), which eliminates node_modules entirely. Instead of installing files into node_modules, PnP stores packages in a .yarn/cache directory and uses a .pnp.cjs file to tell Node.js where to find them. The result is faster installs and smaller repositories, because packages are not duplicated in node_modules .

The PnP trade-off. PnP breaks the assumption that node_modules exists, which many tools rely on. Some packages and toolchains do not work with PnP without configuration. For this reason, many projects that use modern yarn enable nodeLinker: node-modules to get the classic layout while still using yarn’s other features.

Workspaces are supported in both modes:

yarn workspace my-package add express
yarn workspaces foreach run build

Auditing uses yarn npm audit in modern yarn .

Running packages without installing uses yarn dlx:

yarn dlx create-react-app my-app

This is the equivalent of npx in npm.


d. Comparing the three

The three package managers differ in storage strategy, speed, disk usage, and strictness.

Aspectnpmpnpmyarn (classic)yarn (modern/PnP)
Lockfilepackage-lock.jsonpnpm-lock.yamlyarn.lockyarn.lock
node_modulesFlat, hoistedNon-flat, symlinkedFlat, hoistedNone (PnP)
Disk sharingNoYes, via storeNoYes, via cache
Phantom depsPossiblePreventedPossibleN/A
CI installnpm cipnpm install --frozen-lockfileyarn install --frozen-lockfileyarn install --immutable
WorkspacesYesYes, strongYesYes
Install scriptsRun by defaultMust be approvedRun by defaultRun by default

Speed. pnpm is generally the fastest, especially on warm caches, because it links files instead of copying them. yarn classic and modern are faster than npm on cold installs due to parallel downloads. npm has improved significantly in recent versions but remains the slowest of the three on large projects .

Disk usage. pnpm uses the least disk space because of its content-addressable store. A hundred projects using the same dependency consume one copy of that dependency, not a hundred. yarn PnP also saves disk space by avoiding node_modules, but it stores packages in a per-project .yarn/cache that is still larger than pnpm’s shared store.

Strictness. pnpm is the strictest: it prevents undeclared imports and requires approval for install scripts. npm and yarn classic are more permissive, which is convenient for compatibility but allows phantom dependencies to creep in.

Ecosystem compatibility. npm has the broadest compatibility because it is the default and everything works with it. yarn is well-supported but less common than npm. pnpm has excellent compatibility for most packages, but some tools that rely on a flat node_modules structure require configuration or do not work at all .


Complete Example Session

# ============================================
# PART 1: NPM INITIALIZATION AND INSTALL
# ============================================
mkdir my-npm-app && cd my-npm-app
npm init -y
npm install express
npm install --save-dev typescript
# ============================================
# PART 2: NPM REPRODUCIBLE INSTALL
# ============================================
npm ci
# ============================================
# PART 3: NPM AUDIT
# ============================================
npm audit
npm audit fix
# ============================================
# PART 4: PNPM INSTALLATION AND USE
# ============================================
npm install -g pnpm
mkdir my-pnpm-app && cd my-pnpm-app
pnpm init
pnpm add express
pnpm add -D typescript
# ============================================
# PART 5: PNPM REPRODUCIBLE INSTALL
# ============================================
pnpm install --frozen-lockfile
# ============================================
# PART 6: PNPM STORE INSPECTION
# ============================================
pnpm store path
# ============================================
# PART 7: YARN CLASSIC INSTALLATION
# ============================================
npm install -g yarn
mkdir my-yarn-app && cd my-yarn-app
yarn init -y
yarn add express
yarn add --dev typescript
# ============================================
# PART 8: YARN REPRODUCIBLE INSTALL
# ============================================
yarn install --frozen-lockfile
# ============================================
# PART 9: YARN DLX FOR TEMPORARY PACKAGES
# ============================================
yarn dlx create-vite my-app --template react
# ============================================
# PART 10: COMPARING LOCKFILES
# ============================================
ls -la
# package-lock.json (npm)
# pnpm-lock.yaml (pnpm)
# yarn.lock (yarn)

These ten parts cover initialization, installation, reproducible installs, auditing, and the differences in lockfiles and commands across the three package managers.


Quick Reference

Command Comparison

Actionnpmpnpmyarn
Initializenpm init -ypnpm inityarn init -y
Install allnpm installpnpm installyarn install
Add dependencynpm i pkgpnpm add pkgyarn add pkg
Add dev dependencynpm i -D pkgpnpm add -D pkgyarn add -D pkg
Removenpm uninstall pkgpnpm remove pkgyarn remove pkg
CI installnpm cipnpm install --frozen-lockfileyarn install --frozen-lockfile
Run scriptnpm run scriptpnpm run scriptyarn run script
Execute oncenpx pkgpnpm dlx pkgyarn dlx pkg
Auditnpm auditpnpm audityarn npm audit

Lockfiles

Package ManagerLockfile
npmpackage-lock.json
pnpmpnpm-lock.yaml
yarnyarn.lock

Storage Strategy

Package Managernode_modulesDisk SharingPhantom Deps
npmFlat, hoistedNoPossible
pnpmNon-flat, symlinkedYes, content-addressable storePrevented
yarn classicFlat, hoistedNoPossible
yarn modernNone (PnP)Yes, .yarn/cacheN/A

Global Store Locations

Package ManagerStore Location
npm$HOME/.npm (cache)
pnpmpnpm store path
yarn$HOME/.cache/yarn (classic)

Best Practices

✅ Do This:

npm ci                                    # Reproducible install in CI
pnpm install --frozen-lockfile            # pnpm CI install
yarn install --frozen-lockfile            # yarn CI install
pnpm add pkg                              # Saves to package.json automatically
pnpm approve-builds                       # Explicitly approve build scripts

❌ Don’t Do This:

npm install pkg --save                    # ❌ --save is default since npm 5
npm install -g express                    # ❌ Global install for project dependency
rm package-lock.json                      # ❌ Destroys reproducibility
pnpm add pkg --save                       # ❌ pnpm saves by default

Common Pitfalls

PitfallWhy It HappensFix
Phantom dependencynpm/yarn hoist transitive depsUse pnpm or import only declared packages
CI install failsLockfile out of sync with package.jsonRun npm install locally and commit lockfile
Disk fullnpm/yarn duplicate deps per projectUse pnpm
pnpm install failsPackage expects flat node_modulesConfigure node-linker or use npm
Yarn PnP tool incompatibilityTool requires node_modulesSet nodeLinker: node-modules
Different versions on different machinesLockfile not committedAlways commit the lockfile

Real-World Examples

1. CI Install with npm

- run: npm ci
- run: npm test

2. CI Install with pnpm

- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile

3. CI Install with yarn

- run: yarn install --frozen-lockfile

4. pnpm Workspace Configuration

# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'

5. Add Dependency to Workspace Package

pnpm add express --filter my-package

6. Patch a Dependency with pnpm

pnpm patch lodash
# edit the files
pnpm patch-commit /tmp/patch-dir

7. Run Package Without Installing

pnpm dlx create-vite my-app

8. Check Dependency Tree

npm ls
pnpm list
yarn why express

9. Audit for Vulnerabilities

npm audit
pnpm audit
yarn npm audit

10. Clean Install from Scratch

rm -rf node_modules
npm ci

Visual

Storage Strategy Comparison

┌──────────────────────────────────────────────────────────────┐
│  npm / yarn classic            pnpm                          │
│                                                              │
│  Project A                     Project A                     │
│  └── node_modules/             └── node_modules/             │
│      └── lodash/                   └── lodash/ (symlink)     │
│          └── (copy)                    │                     │
│                                        ▼                     │
│  Project B                     Global Store                  │
│  └── node_modules/             └── content-addressable       │
│      └── lodash/                   store                     │
│          └── (copy)                    │                     │
│                                        └── lodash (hard link)│
│  Two copies on disk            One copy on disk              │
└──────────────────────────────────────────────────────────────┘

Lockfile and Reproducibility

┌──────────────────────────────────────────────────────────────┐
│  package.json                                                │
│  └── "express": "^4.18.0"  (range)                           │
│                                                              │
│  Lockfile                                                    │
│  └── express@4.18.2 (exact version + integrity hash)         │
│                                                              │
│  npm ci / pnpm install --frozen-lockfile                     │
│  └── Installs exactly 4.18.2                                 │
│                                                              │
│  Without lockfile: installs latest 4.x                       │
│  With lockfile: installs exactly what was tested             │
└──────────────────────────────────────────────────────────────┘

Phantom Dependencies

┌──────────────────────────────────────────────────────────────┐
│  npm flat node_modules                                        │
│  └── node_modules/                                           │
│      ├── express/           ← declared                        │
│      ├── lodash/            ← hoisted transitive              │
│      └── your-code.js       ← can import lodash               │
│                                 (phantom dependency)          │
│                                                              │
│  pnpm non-flat node_modules                                   │
│  └── node_modules/                                           │
│      ├── express/           ← declared only                   │
│      └── your-code.js       ← cannot import lodash            │
│                                 (prevents phantom deps)      │
└──────────────────────────────────────────────────────────────┘

Choosing a Package Manager

┌──────────────────────────────────────────────────────────────┐
│  New project?                                                │
│       │                                                      │
│       ├── Want fastest, most disk-efficient ──▶ pnpm         │
│       │                                                      │
│       ├── Want maximum compatibility ──▶ npm                 │
│       │                                                      │
│       └── Want mature monorepo support ──▶ pnpm or yarn      │
│                                                              │
│  Existing project?                                           │
│       │                                                      │
│       ├── npm ──▶ Stay, or migrate to pnpm for speed         │
│       │                                                      │
│       ├── yarn classic ──▶ Stay or migrate to pnpm           │
│       │                                                      │
│       └── yarn PnP ──▶ Stay if tooling works                 │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
npmDefault, ships with Node.js, flat node_modules
pnpmContent-addressable store, hard links, non-flat node_modules
yarnClassic (flat) and Modern (PnP) modes
npm lockfilepackage-lock.json
pnpm lockfilepnpm-lock.yaml
yarn lockfileyarn.lock
npm CI installnpm ci
pnpm CI installpnpm install --frozen-lockfile
yarn CI installyarn install --frozen-lockfile
pnpm disk savingsOne copy per version, shared across projects
pnpm strictnessPrevents phantom dependencies
Yarn PnPEliminates node_modules entirely

Key takeaways:

  • npm is the default and has the broadest compatibility. It uses a flat node_modules tree with hoisting and package-lock.json for reproducible installs .
  • pnpm is the fastest and most disk-efficient. It stores every file once in a content-addressable store and hard-links files into each project, saving disk space and install time .
  • pnpm’s non-flat node_modules prevents phantom dependencies. Code cannot import a package that is not declared in package.json, which eliminates a class of bugs that npm’s hoisting allows .
  • yarn offers a classic mode and a modern Plug’n’Play mode. PnP eliminates node_modules but breaks tools that expect it. Most projects that use modern yarn set nodeLinker: node-modules for compatibility .
  • All three use lockfiles for reproducibility. The lockfile records exact versions and integrity hashes. Commit it to version control and use the CI install command that respects it .
  • Use npm ci or its equivalent in CI. It installs exactly what the lockfile specifies and fails if package.json and the lockfile are out of sync, preventing drift .
  • pnpm is the recommended choice for new projects. It is stable, fast, disk-efficient, and strict about dependencies, with strong monorepo support .

Remember: The three package managers all install from the same npm registry and all use lockfiles for reproducibility. The differences are in storage strategy and strictness. npm and yarn classic copy dependencies into each project’s node_modules, which is simple and compatible but wastes disk space and allows phantom dependencies. pnpm stores packages once in a global content-addressable store and hard-links them into each project, which saves disk space and prevents undeclared imports. yarn’s modern PnP mode goes further by eliminating node_modules entirely, at the cost of tool compatibility. For new projects, pnpm is the recommended default: it is fast, efficient, and strict. For existing projects, npm and yarn classic remain perfectly functional, and migrating is optional. What matters most is committing the lockfile and using the CI install command that respects it, because reproducible installs are the foundation of reliable builds.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!