Package management bugs are rarely about forgetting a CLI flag — they come from mixed lockfiles, floating ranges without CI discipline, unpinned managers, and silent transitive upgrades. These practices apply whether you use npm, Yarn, pnpm, or Bun; manager-specific deep dives live under compare.
✓
best practice
Treat the lockfile as source of truth, the manager version as infrastructure, and dependency adds as code review events.
Dependency Hygiene
Every dependency is attack surface, install time, and upgrade debt. Prefer fewer direct dependencies; vendor tiny helpers when a package exists only to save ten lines.
Practice
Why
How
Split deps vs devDeps
Smaller prod images
Install tools with -D / --save-dev
Declare what you import
Avoid phantom deps
Especially under pnpm / Yarn PnP
Remove unused
Less audit noise
knip / depcheck periodically
Prefer maintained packages
Security patches
Check downloads, commits, advisories
Pin risky majors
Breaking changes
Exact or tilde for critical libs
hygiene.sh
Bash
1
npm ls --depth=0
2
npm outdated
3
npx knip
4
# After removing dead code paths:
5
npm uninstall left-pad moment
Commit the Lockfile
Always commit package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lock for applications and most libraries that ship reproducible CI. Delete competing lockfiles.
ci-install.sh
Bash
1
# npm
2
npm ci
3
4
# pnpm
5
pnpm install --frozen-lockfile
6
7
# Yarn Berry
8
yarn install --immutable
9
10
# Bun
11
bun install --frozen-lockfile
✕
danger
Running plain npm install in CI without a lockfile (or ignoring drift) is how production silently picks new transitive majors on Tuesday.
Do
Don't
Commit one lockfile
Commit npm + yarn + pnpm locks together
Review lockfile diffs in PRs
Hide lockfiles from review as "noise"
Regenerate intentionally
Hand-edit integrity hashes
Exact vs Ranges
Ranges in package.json control what new installs may resolve when the lockfile is regenerated. The lockfile still pins exact trees for day-to-day installs.
Strategy
Example
When
^ caret
^4.18.2
Default for most libs; allow minor/patch
~ tilde
~4.18.2
Only patch; cautious apps
Exact
4.18.2
Critical runtime / compliance
Catalog / workspace protocol
catalog: / workspace:*
Monorepos with shared versions
package.json
JSON
1
{
2
"dependencies": {
3
"express": "^4.21.0",
4
"next": "15.1.0",
5
"zod": "~3.24.0"
6
}
7
}
ℹ
info
Automate upgrades with Dependabot/Renovate against the lockfile. Ranges alone do not update production without a deliberate install that rewrites the lock.
Overrides & Resolutions
Force a transitive version when a parent has not bumped yet — typically for security fixes.
npm overrides
JSON
1
{
2
"overrides": {
3
"lodash": "4.17.21",
4
"foo": {
5
".": "1.0.0",
6
"bar": "2.0.0"
7
}
8
}
9
}
Yarn resolutions
JSON
1
{
2
"resolutions": {
3
"lodash": "4.17.21"
4
}
5
}
pnpm overrides
JSON
1
{
2
"pnpm": {
3
"overrides": {
4
"lodash": "4.17.21"
5
}
6
}
7
}
⚠
warning
Overrides are temporary debt. Track them; remove when upstream ranges absorb the fix. Overusing overrides can hide incompatible APIs.
Security Audit
audit.sh
Bash
1
npm audit
2
npm audit --omit=dev
3
npm audit fix # careful: may bump ranges
4
npm audit fix --force # may major-bump — review!
5
6
pnpm audit
7
yarn npm audit # Yarn Berry via npm protocol
8
bun pm ls # inspect; use advisory tooling too
Severity
Response
Critical / High in prod path
Patch same day; override if needed
Dev-only tooling
Schedule; still fix before public CI compromise vectors
No fix available
Replace package, vendor, or isolate
🔥
pro tip
Combine npm audit with GitHub Dependabot alerts and lockfile PRs. Audit alone misses malicious brand-new packages that have no CVE yet — review new dependencies carefully.
only-allow
Prevent accidental npm install in a pnpm/Yarn/Bun repo (and vice versa).