|$ curl https://forge-ai.dev/api/markdown?path=docs/npm/best-practices
$cat docs/npm-best-practices.md
updated Today·24-32 min read·published

npm Best Practices

◆npm◆Security◆Best Practices◆Intermediate🎯Free Tools
Introduction

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.

PracticeWhyHow
Split deps vs devDepsSmaller prod imagesInstall tools with -D / --save-dev
Declare what you importAvoid phantom depsEspecially under pnpm / Yarn PnP
Remove unusedLess audit noiseknip / depcheck periodically
Prefer maintained packagesSecurity patchesCheck downloads, commits, advisories
Pin risky majorsBreaking changesExact or tilde for critical libs
hygiene.sh
Bash
1npm ls --depth=0
2npm outdated
3npx knip
4# After removing dead code paths:
5npm 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
2npm ci
3
4# pnpm
5pnpm install --frozen-lockfile
6
7# Yarn Berry
8yarn install --immutable
9
10# Bun
11bun 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.
DoDon't
Commit one lockfileCommit npm + yarn + pnpm locks together
Review lockfile diffs in PRsHide lockfiles from review as "noise"
Regenerate intentionallyHand-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.

StrategyExampleWhen
^ caret^4.18.2Default for most libs; allow minor/patch
~ tilde~4.18.2Only patch; cautious apps
Exact4.18.2Critical runtime / compliance
Catalog / workspace protocolcatalog: / 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
1npm audit
2npm audit --omit=dev
3npm audit fix # careful: may bump ranges
4npm audit fix --force # may major-bump — review!
5
6pnpm audit
7yarn npm audit # Yarn Berry via npm protocol
8bun pm ls # inspect; use advisory tooling too
SeverityResponse
Critical / High in prod pathPatch same day; override if needed
Dev-only toolingSchedule; still fix before public CI compromise vectors
No fix availableReplace 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).

package.json
JSON
1{
2 "packageManager": "pnpm@9.15.0",
3 "scripts": {
4 "preinstall": "npx only-allow pnpm"
5 }
6}

More lifecycle detail: Package Lifecycle & Hooks. Pin managers with Corepack.

engines Field

Declare supported Node (and optionally npm) versions. Enforce with engine-strict so CI fails early on wrong runtimes.

package.json
JSON
1{
2 "engines": {
3 "node": ">=20 <23",
4 "pnpm": ">=9 <10"
5 }
6}
.npmrc
INI
1engine-strict=true
📝

note

engines is advisory unless strict mode is on. Pair with CI matrix testing on the versions you claim to support.
Private Registries

Scope private packages to your registry; keep the public registry for everything else. Never commit tokens — use CI secrets and user-level auth.

.npmrc
INI
1@acme:registry=https://npm.pkg.github.com
2//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
3always-auth=true
auth.sh
Bash
1# Local (user npmrc — not committed)
2npm login --scope=@acme --registry=https://npm.pkg.github.com
3
4# CI
5echo "//npm.pkg.github.com/:_authToken=${NPM_TOKEN}" >> .npmrc
RiskMitigation
Token in gitEnv substitution; secret scanners
Dependency confusionScoped names; internal-only publish
Mirror driftPin registry URLs in committed .npmrc
Monorepo Hygiene
  • One root lockfile; packages use workspace protocol for internal deps.
  • Share versions via pnpm catalogs or Yarn constraints — avoid copy-paste drift.
  • Filter CI to changed packages + dependents.
  • Do not publish private apps; mark "private": true.
  • Keep package boundaries clear: UI kit should not depend on the Next app.
  • Prefer pnpm or Yarn Berry for large graphs — see pnpm / workspaces.
internal-dep
JSON
1{
2 "name": "@acme/web",
3 "dependencies": {
4 "@acme/ui": "workspace:*"
5 }
6}
ci-filter.sh
Bash
1pnpm install --frozen-lockfile
2pnpm --filter "...[origin/main]" run lint test build
Scripts Discipline
DoWhy
Keep scripts cross-platform or document Unix-onlyWindows contributors / CI images differ
Avoid heavy logic in postinstallSecurity + slow installs
Use ignore-scripts in untrusted contextsSupply-chain defense
Name scripts consistently (lint, test, build)Monorepo foreach / filter
safe-install.sh
Bash
1npm ci --ignore-scripts
2# then explicitly build trusted natives if required
Publishing Hygiene
package.json
JSON
1{
2 "files": ["dist"],
3 "sideEffects": false,
4 "publishConfig": {
5 "access": "public",
6 "provenance": true
7 }
8}
  • Use files / .npmignore so secrets and tests stay out of the tarball.
  • Enable provenance on public packages when using supported CI.
  • Run npm pack --dry-run before first publish.

See Publishing Packages.

Team Checklist
  • One package manager + Corepack packageManager
  • One committed lockfile; frozen installs in CI
  • only-allow + engines + engine-strict
  • Dependabot/Renovate for routine bumps
  • Audit critical path on a schedule
  • Review new dependencies like code
  • Document private registry auth for humans and CI
  • Monorepo: catalogs/constraints + filtered pipelines
✓

best practice

If a PR only changes the lockfile, still require CI green — transitive updates break builds more often than application code.

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.