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.
| Aspect | npm | pnpm | yarn (classic) | yarn (modern/PnP) |
|---|---|---|---|---|
| Lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock | yarn.lock |
node_modules | Flat, hoisted | Non-flat, symlinked | Flat, hoisted | None (PnP) |
| Disk sharing | No | Yes, via store | No | Yes, via cache |
| Phantom deps | Possible | Prevented | Possible | N/A |
| CI install | npm ci | pnpm install --frozen-lockfile | yarn install --frozen-lockfile | yarn install --immutable |
| Workspaces | Yes | Yes, strong | Yes | Yes |
| Install scripts | Run by default | Must be approved | Run by default | Run 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
| Action | npm | pnpm | yarn |
|---|---|---|---|
| Initialize | npm init -y | pnpm init | yarn init -y |
| Install all | npm install | pnpm install | yarn install |
| Add dependency | npm i pkg | pnpm add pkg | yarn add pkg |
| Add dev dependency | npm i -D pkg | pnpm add -D pkg | yarn add -D pkg |
| Remove | npm uninstall pkg | pnpm remove pkg | yarn remove pkg |
| CI install | npm ci | pnpm install --frozen-lockfile | yarn install --frozen-lockfile |
| Run script | npm run script | pnpm run script | yarn run script |
| Execute once | npx pkg | pnpm dlx pkg | yarn dlx pkg |
| Audit | npm audit | pnpm audit | yarn npm audit |
Lockfiles
| Package Manager | Lockfile |
|---|---|
| npm | package-lock.json |
| pnpm | pnpm-lock.yaml |
| yarn | yarn.lock |
Storage Strategy
| Package Manager | node_modules | Disk Sharing | Phantom Deps |
|---|---|---|---|
| npm | Flat, hoisted | No | Possible |
| pnpm | Non-flat, symlinked | Yes, content-addressable store | Prevented |
| yarn classic | Flat, hoisted | No | Possible |
| yarn modern | None (PnP) | Yes, .yarn/cache | N/A |
Global Store Locations
| Package Manager | Store Location |
|---|---|
| npm | $HOME/.npm (cache) |
| pnpm | pnpm 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Phantom dependency | npm/yarn hoist transitive deps | Use pnpm or import only declared packages |
| CI install fails | Lockfile out of sync with package.json | Run npm install locally and commit lockfile |
| Disk full | npm/yarn duplicate deps per project | Use pnpm |
| pnpm install fails | Package expects flat node_modules | Configure node-linker or use npm |
| Yarn PnP tool incompatibility | Tool requires node_modules | Set nodeLinker: node-modules |
| Different versions on different machines | Lockfile not committed | Always 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
| Item | Value |
|---|---|
| npm | Default, ships with Node.js, flat node_modules |
| pnpm | Content-addressable store, hard links, non-flat node_modules |
| yarn | Classic (flat) and Modern (PnP) modes |
| npm lockfile | package-lock.json |
| pnpm lockfile | pnpm-lock.yaml |
| yarn lockfile | yarn.lock |
| npm CI install | npm ci |
| pnpm CI install | pnpm install --frozen-lockfile |
| yarn CI install | yarn install --frozen-lockfile |
| pnpm disk savings | One copy per version, shared across projects |
| pnpm strictness | Prevents phantom dependencies |
| Yarn PnP | Eliminates node_modules entirely |
Key takeaways:
- npm is the default and has the broadest compatibility. It uses a flat
node_modulestree with hoisting andpackage-lock.jsonfor 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_modulesprevents phantom dependencies. Code cannot import a package that is not declared inpackage.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_modulesbut breaks tools that expect it. Most projects that use modern yarn setnodeLinker: node-modulesfor 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 cior its equivalent in CI. It installs exactly what the lockfile specifies and fails ifpackage.jsonand 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!