DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/Foundations & Tooling/Package Managers (npm, yarn, pnpm)
beginner20 min read·Updated Dec 2024Expert reviewed

Package Managers (npm, yarn, pnpm)

npm, yarn, and pnpm all install JavaScript packages — but their approaches to lockfiles, disk storage, monorepo support, and phantom dependencies differ in ways that matter at scale. Knowing when to use each and what can go wrong saves hours of debugging CI failures.

npmyarnpnpmpackageslockfilesemvermonoreponode_modules

Knowledge Check

3 questions · pass at 70%

0/3
easymcq

1. What does `^18.2.0` mean in `package.json`?


Interview Questions

1 questions

Cheatsheet

Download-ready reference
Cheatsheet
Semver ranges:
  ^18.2.0   >=18.2.0 <19.0.0  (minor + patch updates)
  ~18.2.0   >=18.2.0 <18.3.0  (patch updates only)
  18.2.0    exactly this version
  *         any version (avoid)

npm:
  npm install            install from package.json (may update lockfile)
  npm ci                 clean install from lockfile (use in CI)
  npm install pkg        add to dependencies
  npm install -D pkg     add to devDependencies
  npm audit              scan for vulnerabilities
  npm run <script>       run package.json script

pnpm (recommended):
  pnpm install --frozen-lockfile   CI install (never updates lockfile)
  pnpm add pkg                     add dependency
  pnpm add -D pkg                  add devDependency
  pnpm --filter @pkg/name build    run script in specific workspace package
  pnpm -r build                    run script in all workspace packages

Key decisions:
  CI: always use ci/frozen-lockfile install, not plain install
  Commit lockfiles: always
  Team: pick one package manager and enforce it
  New projects: pnpm for monorepos + disk efficiency
  Phantom deps: npm/yarn allow them; pnpm prevents them

node_modules/ structure:
  npm/yarn: flat (hoisted) — phantom deps possible
  pnpm:     symlinked from store — only declared deps accessible

Related topics

Build Tools (Vite, Webpack, esbuild, Rollup, Turbopack)ProModule Systems (ESM vs CommonJS)ProMonorepo (Nx, Turborepo)Pro

On this page

20m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

Why package managers exist#

JavaScript modules have external dependencies. A package manager:

  1. Resolves which version of each package to install (respecting semver ranges)
  2. Downloads packages from a registry (npm, JSR, or private)
  3. Links them into node_modules so require() / import can find them
  4. Writes a lockfile so the exact resolved versions are reproducible across machines

Semantic Versioning (semver)#

Every package follows MAJOR.MINOR.PATCH:

  • MAJOR (1.x.x → 2.x.x): breaking changes
  • MINOR (1.1.x → 1.2.x): new features, backwards-compatible
  • PATCH (1.1.1 → 1.1.2): bug fixes, backwards-compatible

Version ranges in package.json:

"react": "^18.2.0"    // >=18.2.0 <19.0.0  (caret: minor+patch updates)
"react": "~18.2.0"    // >=18.2.0 <18.3.0  (tilde: patch updates only)
"react": "18.2.0"     // exactly 18.2.0
"react": ">=18.0.0"   // any 18+
"react": "*"          // any version (dangerous)

The lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) pins every resolved version exactly. Without a lockfile in CI, ^18.2.0 today might resolve to 18.3.0 tomorrow after a new release — breaking your build.

npm (Node Package Manager)#

The default. Ships with Node.js. Registry: registry.npmjs.org.

npm install              # install all deps from package.json
npm install react        # install + add to dependencies
npm install -D typescript # install + add to devDependencies
npm install -g nodemon   # install globally
npm ci                   # clean install from lockfile (for CI)
npm update               # update to latest within semver ranges
npm audit                # check for known vulnerabilities
npm run build            # run the "build" script from package.json

Key files:

  • package.json — manifest (name, version, scripts, deps)
  • package-lock.json — exact resolved versions (v2 format: npm 7+; v3: npm 10+)
  • .npmrc — config (registry URL, auth tokens, cache path)

npm ci vs npm install: Always use npm ci in CI pipelines. It:

  • Fails if package-lock.json is missing or mismatched
  • Deletes node_modules first for a clean install
  • Never updates the lockfile
  • Is faster than npm install in CI

yarn (Classic/Berry)#

Yarn Classic (v1) was created by Facebook in 2016 to address npm v3's non-deterministic installs and lack of a lockfile. It introduced yarn.lock and parallel downloads.

yarn          # install all deps
yarn add react         # add dependency
yarn add -D typescript # add devDependency
yarn remove lodash     # remove
yarn upgrade           # update deps

Yarn Berry (v2/v3/v4) introduced Plug'n'Play (PnP):

  • No node_modules folder
  • Packages stored in .yarn/cache as zip files
  • require() intercepted by a .pnp.cjs resolver
  • Zero-installs: commit .yarn/cache to git — yarn install becomes instant in CI

PnP is powerful but requires all tools (IDEs, bundlers) to support it. Many teams stay on Yarn Classic for compatibility.

pnpm#

pnpm's key innovation: a content-addressable store on disk, shared across all projects.

pnpm install      # install all deps
pnpm add react    # add dependency
pnpm add -D vitest # add devDependency
pnpm remove lodash
pnpm dlx create-vite  # equivalent of npx

pnpm's node_modules structure

Instead of copying packages into node_modules, pnpm creates hard links from a central store (~/.pnpm-store). If 10 projects use React 18.2.0, there's only one copy on disk. Each project's node_modules has hard links pointing to the store.

This has two major effects:

  1. Disk space: installing the same package across multiple projects is cheap
  2. Phantom dependency prevention: packages are not hoisted by default. Your code can only import packages you've explicitly declared as dependencies

Phantom dependencies — the pnpm killer feature

With npm/yarn, node_modules is flat — packages your dependencies depend on are hoisted to the top level. This means you can accidentally import a package you didn't declare:

// You didn't add "lodash" to package.json
// But "some-lib" depends on it, so it's in node_modules
import _ from 'lodash';  // works in npm/yarn, breaks randomly when some-lib updates

pnpm prevents this: only your declared dependencies are in the direct node_modules.

pnpm workspaces

pnpm has excellent monorepo support:

# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
pnpm --filter @myapp/ui build   # run build only in @myapp/ui
pnpm -r build                   # run build in all packages

Comparison#

Featurenpmyarn classicyarn berrypnpm
Lockfilepackage-lock.jsonyarn.lockyarn.lockpnpm-lock.yaml
SpeedMediumFastFastest (PnP)Fast
Disk usageHighHighLow (cache)Low (shared store)

Common Mistakes#

1. Not committing the lockfile#

Lockfiles ensure reproducible installs. Never .gitignore your lockfile. Every engineer and CI machine gets the exact same package versions.

2. Mixing package managers#

Each manager has its own lockfile format. Running npm install in a project with yarn.lock creates a package-lock.json that may conflict. Pick one and enforce it with a .npmrc or engine check.

// package.json — prevent accidental use of wrong manager
{
  "engines": { "node": ">=20", "pnpm": ">=8" },
  "packageManager": "pnpm@8.15.0"
}

3. Using * or very wide version ranges#

"lodash": "*" will install any version, including a major breaking change. Use ^ or pin exactly in critical dependencies.

4. Global installs for project tools#

npm install -g prettier means the version is machine-specific. Anyone else on the project might run a different Prettier version and produce different formatting. Install dev dependencies locally and run via npx or package.json scripts.

5. Installing production deps as devDependencies (or vice versa)#

dependencies are bundled into the production build. devDependencies are not (for some tools/environments). TypeScript, ESLint, testing libraries → devDependencies. Runtime libs like React, axios → dependencies.


Best Practices#

  • Always commit the lockfile. Run npm ci / pnpm install --frozen-lockfile in CI.
  • Run npm audit / pnpm audit in CI. Fail the build on high-severity vulnerabilities.
  • Use pnpm for new projects — better disk efficiency and phantom-dependency protection.
  • Pin major versions: use 18.x.x ranges, not *. Update majors deliberately.
  • Keep node_modules out of docker layers: copy only + lockfile first, install, then copy source. Docker caches the install layer unless deps change.

Performance Tips#

  • npm ci > npm install in CI: 2–3x faster because it skips resolution.
  • Restore node_modules from cache between CI runs using the lockfile hash as the cache key. GitHub Actions example: cache: 'npm' in the setup-node action.
  • pnpm's store is ideal for monorepos — one store for all packages, deduplicated on disk.
  • Use --prefer-offline in CI if your registry is unreliable — falls back to local cache.

Monorepo
workspaces
workspaces
workspaces
workspaces (best)
Phantom depsAllowedAllowedPreventedPrevented
Peer dep handlingAuto-install (npm 7+)ManualManualStrict
Node.js defaultYesNoNoNo
package.json
  • Use engines field to declare which Node/pnpm versions the project requires.