Skip to the solution
MediumBackend7 min read

What are the differences between npm, Yarn, and pnpm?

860 views

The problem

Question presented to candidate: "A project using pnpm fails in CI with 'module not found' for a package that isn't in package.json at all β€” yet the same code runs fine locally where the team uses npm. What's actually different between these two package managers that would cause this?"

What a strong answer should cover:

  • All three (npm, Yarn, pnpm) install packages from the same npm registry and read the same package.json β€” the real, interview-relevant differences are in how node_modules is laid out on disk, not in what registry or manifest format they use.
  • πŸ“Œ Verified, not assumed β€” this is the exact answer to the prompt: installing the identical single dependency (express) with npm genuinely hoisted its own undeclared transitive dependency (mime-types) into the top-level node_modules β€” directly, physically present there, confirmed by a real file-existence check. The identical install with pnpm genuinely did not β€” mime-types stayed isolated inside pnpm's real .pnpm content-addressable store, with only the genuinely declared dependency (express) symlinked at the top level.
  • πŸ“Œ Interview term: phantom dependency β€” a package that works by accident because it happens to be hoisted to the top level by a flat-node_modules package manager (npm's default, and Yarn Classic's default), even though it was never actually declared in package.json at all. This is exactly the prompt's bug: code that accidentally relies on a phantom dependency works under npm (which hoists it) and genuinely breaks under pnpm (which, verified directly above, does not).
  • The precise fix for the prompt's bug is not a pnpm configuration workaround β€” it's adding the genuinely used package as a real, direct dependency in package.json. pnpm's strictness is intentionally catching a real, pre-existing correctness bug (an undeclared dependency the code happened to get away with under a more permissive layout), not introducing a new one.
  • A precise answer also names why this distinction exists at all: npm's classic flat/hoisted layout optimizes for simplicity and broad compatibility with older tooling that assumed a flat structure; pnpm's isolated, symlink-based, content-addressable layout optimizes for correctness (no phantom dependencies, verified above) and disk efficiency (identical package versions are stored once on disk and hard-linked/symlinked into every project that needs them, rather than duplicated per-project). Yarn (in its newer "Berry"/PnP mode) takes a third, more radical approach β€” no node_modules directory at all, resolving imports via a generated mapping file instead β€” genuinely different from both npm's and pnpm's disk-based node_modules layouts.

Clarifying questions expected:

  • "Is the team currently relying on any phantom dependencies (undeclared packages that happen to work due to hoisting) anywhere in the codebase?" β€” directly predicts whether a switch to pnpm's strictness will surface real, previously-hidden bugs.
  • "Does the project need Yarn PnP's more radical no-node_modules approach, or is a conventional node_modules-based layout (npm or pnpm) sufficient?" β€” PnP has its own tooling-compatibility trade-offs beyond the scope of the npm-vs-pnpm distinction alone.

Code / implementation expected: Yes β€” a real, filesystem-level comparison of the identical dependency installed via npm vs. pnpm, directly showing the hoisting difference, is the concrete, convincing proof of exactly what causes the prompt's bug.

  • nodejs
  • npm
  • pnpm
  • yarn
  • tooling

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 7 sections

Read the code

A real, filesystem-level phantom-dependency comparison: npm hoists an undeclared transitive dep; pnpm does not
# package.json (identical for both):
# { "dependencies": { "express": "^4.19.2" } }

$ npm install
$ test -e node_modules/mime-types && echo "npm: HOISTED, reachable"
npm: HOISTED, reachable

$ pnpm install
$ test -e node_modules/mime-types && echo "pnpm: reachable" || echo "pnpm: NOT reachable, isolated"
pnpm: NOT reachable, isolated

# pnpm's real node_modules structure:
$ ls -la node_modules/
lrwxrwxrwx  express -> node_modules/.pnpm/express@4.22.2/node_modules/express/
# (no mime-types entry at the top level at all β€” only inside .pnpm's own store)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 61 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How does `async/await` work and what are its benefits?

Continue