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.
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
"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.
The default. Ships with Node.js. Registry: registry.npmjs.org.
npm install # install all deps from package.jsonnpm install react # install + add to dependenciesnpm install -D typescript # install + add to devDependenciesnpm install -g nodemon # install globallynpm ci # clean install from lockfile (for CI)npm update # update to latest within semver rangesnpm audit # check for known vulnerabilitiesnpm run build # run the "build" script from package.json
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.
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:
Disk space: installing the same package across multiple projects is cheap
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_modulesimport _ 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.
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"}
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.
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 deps
Allowed
Allowed
Prevented
Prevented
Peer dep handling
Auto-install (npm 7+)
Manual
Manual
Strict
Node.js default
Yes
No
No
No
package.json
Use engines field to declare which Node/pnpm versions the project requires.