cheat sheet

npm Package Manager

Complete npm reference — installing packages, scripts, workspaces, package.json anatomy, audit, publish, link, lockfiles, and registry configuration.

npm Package Manager

What it is

npm (Node Package Manager) is the default package manager bundled with Node.js. It manages project dependencies declared in package.json, installs packages from the public npm registry at npmjs.com, and provides a script runner for common development tasks. npm also hosts the world's largest software registry with over 2 million packages.

Install

npm ships with Node.js. Verify you have it:

bash
npm --version

Output:

text
10.9.2

Upgrade npm itself to the latest version:

bash
npm install -g npm@latest

Output: (none — exits 0 on success)

Initialize a project

bash
# Interactive prompts
npm init

# Skip all prompts and accept defaults
npm init -y

Output (npm init -y):

text
Wrote to /home/user/myproject/package.json:

{
  "name": "myproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Installing packages

bash
# Install a package and save to dependencies (default)
npm install express
npm install express react react-dom

# Save to devDependencies
npm install --save-dev eslint prettier typescript
npm install -D vitest

# Install exact version (no version range in package.json)
npm install --save-exact lodash
npm install -E lodash

# Install globally
npm install -g typescript
npm install -g nodemon

# Install from a specific version / tag / range
npm install react@18
npm install react@^18.0.0
npm install react@latest
npm install react@next

# Install all dependencies listed in package.json
npm install
npm ci        # clean install — uses lockfile exactly, faster in CI

Output: (none — exits 0 on success)

Uninstalling packages

bash
npm uninstall express
npm uninstall --save-dev eslint
npm uninstall -g typescript

Output: (none — exits 0 on success)

package.json anatomy

code
{
  "name": "my-app",
  "version": "1.2.3",
  "description": "A sample application",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "engines": {
    "node": ">=20.0.0"
  },
  "scripts": {
    "start": "node dist/server.js",
    "dev": "node --watch src/server.js",
    "build": "tsc",
    "test": "vitest",
    "lint": "eslint src",
    "format": "prettier --write src"
  },
  "dependencies": {
    "express": "^4.21.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "vitest": "^1.6.0",
    "eslint": "^9.0.0"
  },
  "peerDependencies": {
    "react": ">=17.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.3"
  },
  "keywords": ["api", "server"],
  "author": "Alice Dev <alice@example.com>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/user/my-app"
  }
}

Dependency types

FieldInstalled whenPurpose
dependenciesAlwaysRuntime — shipped to users
devDependenciesNot in production (npm ci --omit=dev)Build tools, linters, test frameworks
peerDependenciesMust be provided by the consumerPlugin / library host packages (e.g., React plugins)
optionalDependenciesInstall attempted but failure is ignoredPlatform-specific native bindings

Running scripts

bash
# Run a named script from the scripts field
npm run build
npm run lint
npm run dev

# Special shortcuts (no "run" needed)
npm start      # runs scripts.start
npm test       # runs scripts.test
npm stop       # runs scripts.stop
npm restart    # runs scripts.restart

# Pass extra arguments after --
npm test -- --watch
npm run build -- --minify

Output: (none — exits 0 on success)

Pre and post hooks run automatically:

json
{
  "scripts": {
    "prebuild": "npm run lint",
    "build": "tsc",
    "postbuild": "echo Build complete"
  }
}

Listing and inspecting packages

bash
# List locally installed packages (one level deep)
npm list
npm list --depth=0

# List globally installed packages
npm list -g --depth=0

# Show info about a specific installed package
npm list express

# Show the full dependency tree
npm list --all

# Show details about a package from the registry
npm info react
npm info react version
npm info react versions --json

Output (npm list --depth=0):

text
my-app@1.2.3 /home/user/my-app
├── express@4.21.0
├── react@18.3.1
└── zod@3.23.8

Checking for outdated packages

bash
npm outdated

Output:

text
Package    Current  Wanted  Latest  Location             Depended by
express    4.18.2   4.21.0  4.21.0  node_modules/express my-app
zod        3.21.0   3.23.8  3.23.8  node_modules/zod     my-app

Updating packages

bash
# Update all packages to their wanted version (respects semver range)
npm update

# Update a specific package
npm update express

# Update to latest (ignores semver range — use with care)
npm install express@latest

Output: (none — exits 0 on success)

Security auditing

bash
# Audit for known vulnerabilities
npm audit

# Fix automatically (safe fixes only — adjusts versions within semver range)
npm audit fix

# Fix including breaking changes (bumps major versions)
npm audit fix --force

# Output as JSON (useful for CI parsing)
npm audit --json

Output (npm audit):

text
# npm audit report

express  <4.19.2
Severity: moderate
Open Redirect in Express - https://github.com/advisories/GHSA-rv95-896h-c2vc
fix available via `npm audit fix`
node_modules/express

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Publishing packages

bash
# Log in to the npm registry
npm login

# Check what will be published (dry run)
npm pack --dry-run

# Create a tarball without publishing
npm pack

# Publish the current package
npm publish

# Publish with a tag (default tag is "latest")
npm publish --tag beta

# Bump the version (updates package.json and creates a git tag)
npm version patch    # 1.2.3 → 1.2.4
npm version minor    # 1.2.3 → 1.3.0
npm version major    # 1.2.3 → 2.0.0
npm version 1.5.0    # set exact version

Output: (none — exits 0 on success)

Link a local package into another project without publishing to the registry:

bash
# In the package you're developing
cd ~/projects/my-library
npm link

# In the project that consumes it
cd ~/projects/my-app
npm link my-library

# When done, unlink
npm unlink my-library

Output: (none — exits 0 on success)

package-lock.json

package-lock.json records the exact version of every installed package and its dependencies. Always commit it to version control.

bash
# Install using exact versions from the lockfile (CI-safe)
npm ci

# Differences between npm install and npm ci:
# - npm ci deletes node_modules before install
# - npm ci fails if package-lock.json is out of sync with package.json
# - npm ci is faster and reproducible

Output: (none — exits 0 on success)

When you have peer dependency conflicts:

bash
# Install ignoring peer dependency errors (use cautiously)
npm install --legacy-peer-deps

Output: (none — exits 0 on success)

Workspaces (monorepos)

json
// Root package.json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Install all workspace dependencies from root
npm install

# Run a script in a specific workspace
npm run build -w packages/my-lib
npm run build --workspace=packages/my-lib

# Run a script in all workspaces
npm run test --workspaces
npm run build --workspaces --if-present

# Install a package into a specific workspace
npm install lodash -w apps/web

# Add a local workspace package as a dependency
npm install my-lib -w apps/web

Output (npm run build --workspaces):

text
> my-lib@1.0.0 build
> tsc

> my-app@0.1.0 build
> next build

✓ Compiled successfully

Registry configuration

bash
# View current registry
npm config get registry

# Set registry globally
npm config set registry https://registry.npmjs.org/

# Set a scoped registry (e.g., for private packages from GitHub)
npm config set @myorg:registry https://npm.pkg.github.com/

# View all config
npm config list

Output: (none — exits 0 on success)

.npmrc file (place in project root or home directory):

ini
# .npmrc
registry=https://registry.npmjs.org/
@myorg:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
save-exact=true
engine-strict=true

Useful flags reference

FlagEffect
--dry-runShow what would happen without doing it
--prefer-offlineUse cached packages when possible
--legacy-peer-depsIgnore peer dependency conflicts
--ignore-scriptsSkip lifecycle scripts (safer for untrusted packages)
--omit=devSkip devDependencies (production install)
--fundShow funding info
--verboseDetailed logging
--silentSuppress all output
--jsonOutput as JSON

Common errors and fixes

ErrorLikely causeFix
EACCES: permission deniedGlobal install without permissionUse nvm or add npm prefix to PATH instead of sudo
ERESOLVE unable to resolve dependency treePeer dependency conflict--legacy-peer-deps or update conflicting packages
Cannot find module 'X'Package not installed or missing from package.jsonnpm install X
npm ERR! code ENOENTRunning npm in wrong directory or missing package.jsonCheck working directory
npm warn deprecatedPackage uses an old APICheck if a maintained replacement exists
code EINTEGRITYCorrupted cache or lockfile hash mismatchnpm cache clean --force && npm ci
npm ERR! 404 Not FoundPackage name typo or private package without authCheck name or configure .npmrc auth

package-lock.json — lockfile internals

package-lock.json is the source of truth for exactly which package versions get installed. It includes resolved URLs, integrity hashes (SHA-512), the dependency tree shape, and metadata about every direct and transitive dependency. Reach for this when you want to understand why npm install produces different trees on different machines, or when you need to audit what actually shipped to production.

Anatomy

json
{
  "name": "my-app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "my-app",
      "version": "1.0.0",
      "dependencies": {
        "express": "^4.19.0"
      }
    },
    "node_modules/express": {
      "version": "4.19.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
      "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlz+...",
      "license": "MIT",
      "dependencies": {
        "accepts": "~1.3.8",
        "body-parser": "1.20.2"
      },
      "engines": { "node": ">= 0.10.0" }
    }
  }
}
FieldMeaning
lockfileVersion1 (npm 5–6), 2 (npm 7+), 3 (npm 7+ optimised)
packagesTree keyed by location relative to project root
resolvedThe exact tarball URL fetched
integritySHA-512 hash verified on every install
requiresLegacy v1 compatibility flag

Lockfile v3 is significantly smaller than v2 — same content, no dependencies block at the top. Modern npm writes v3 by default. Convert by deleting the lockfile and running npm install.

npm install vs npm ci

These two commands look interchangeable but behave differently. npm install reconciles package.json and the lockfile, updating the latter as needed. npm ci ("clean install") refuses to touch the lockfile — it's the right tool for CI and reproducible Docker builds.

bash
# npm install — read-write on the lockfile
npm install              # may update package-lock.json if it's stale

# npm ci — read-only on the lockfile, deletes node_modules first
npm ci                   # fails if package.json and lockfile disagree

Output (npm ci with a mismatch):

text
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json
npm error or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install`
npm error before continuing.
npm error
npm error Missing: express@4.19.2 from lock file

Lockfile diff hygiene

Lockfile diffs in PRs are notoriously large. To make them manageable:

bash
# Show only meaningful changes (versions, not metadata churn)
git diff package-lock.json | grep -E '^\+.*"version"' | head -20

# Detect when only the lockfile changed (no package.json) — usually a stale lockfile
git diff --name-only HEAD~ | grep -q '^package.json$' || \
  echo "Only lockfile changed — run 'npm install' locally"

Output: (none — exits 0 on success)

Hoisting and node_modules layout

npm uses flat hoisting: every package that doesn't conflict by version is lifted to the top-level node_modules. Transitive dependencies are accessible to any other package that resolves them on the import path — sometimes by accident.

text
my-app/
└── node_modules/
    ├── express/             # direct dep
    ├── body-parser/         # transitive (express needs it) — hoisted to top
    ├── accepts/             # transitive — hoisted
    ├── react/               # direct dep
    └── lodash/              # could be transitive of either; hoisted

This means require("body-parser") in your code "works" even if you didn't add it to package.json — until the day Express drops it as a dependency and your app silently breaks. The fix is strict linting (eslint-plugin-import with no-extraneous-dependencies) or switching to pnpm / Yarn PnP. See pnpm.

Inspect hoisting:

bash
npm list --all --parseable | head -10

Output:

text
/home/alice/my-app
/home/alice/my-app/node_modules/express
/home/alice/my-app/node_modules/body-parser
/home/alice/my-app/node_modules/accepts
/home/alice/my-app/node_modules/react

Force a deduplication pass — collapse duplicate versions and reuse a single copy where semver ranges allow:

bash
npm dedupe

Output:

text
removed 8 packages, and audited 247 packages in 1s

Lifecycle scripts and hooks

npm runs lifecycle scripts at predictable points during install, publish, and ad-hoc npm run invocations. Use them for build steps, codegen, and migrations — but be wary of postinstall in published packages (it's the classic supply-chain attack vector).

Standard lifecycle events

EventWhen it firesTypical use
preinstallBefore deps are installedValidate Node version, set env
install / postinstallAfter deps are installedNative build (node-gyp), patch source
prepareBefore pack/publish, after installTypeScript compile, copy assets
prepack / postpackAround tarball generationModify files for publish
prepublishOnlyBefore publish (not on install)Final checks, build, tests
prepublish (deprecated)Before publish + before installAvoid — confusing semantics
publish / postpublishAfter publish completesNotify, tag, push docs
preversion / postversionAround npm versionRun tests, push tag
pre<name> / post<name>Around any custom scriptGeneric hook
json
{
  "scripts": {
    "preinstall": "node --version | grep -q v22 || (echo 'Need Node 22'; exit 1)",
    "build": "tsc",
    "prebuild": "npm run lint",
    "postbuild": "node scripts/copy-assets.mjs",
    "prepare": "husky install && npm run build",
    "prepublishOnly": "npm run build && npm test",
    "postpublish": "git push --follow-tags"
  }
}

Disabling lifecycle scripts

Untrusted packages (anything you've not audited) can ship a malicious postinstall. Disable scripts when you're auditing a new dep:

bash
# Skip ALL lifecycle scripts for this install
npm install --ignore-scripts

# Skip scripts globally via .npmrc
echo "ignore-scripts=true" >> ~/.npmrc

Output: (none — exits 0 on success)

npm install --ignore-scripts is a security best practice for global tools you don't trust yet. After installing, audit the package and re-run npm rebuild to execute scripts deliberately.

.npmrc configuration reference

.npmrc is npm's config file. npm reads from four locations in order — later wins:

  1. ~/.npmrc — per-user
  2. <prefix>/etc/npmrc — global (where npm prefix -g points)
  3. ./.npmrc — per-project
  4. Environment variables (NPM_CONFIG_<KEY>=<value>)
ini
# .npmrc — common keys
registry=https://registry.npmjs.org/
@my-org:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

# Install behaviour
save-exact=true               # don't write ^ ranges
save-prefix=~                 # use ~ instead of ^ for new deps
engine-strict=true            # fail if engines.node doesn't match
prefer-offline=false          # try network first; cache as fallback
fund=false                    # suppress funding messages
audit-level=moderate          # fail npm install on moderate+ vulns

# Logging and performance
loglevel=warn                 # silent | error | warn | notice | http | timing | info | verbose | silly
maxsockets=50                 # parallel connections to the registry
fetch-retries=5
fetch-retry-mintimeout=10000
fetch-retry-maxtimeout=60000

# Cache and prefix
cache=~/.npm
prefix=~/.npm-global          # where global installs go (avoid /usr/lib without sudo)

# Auth and proxies
//proxy.example.com:8080/:_authToken=${PROXY_TOKEN}
proxy=http://proxy.example.com:8080
https-proxy=http://proxy.example.com:8080
noproxy=localhost,.internal.example.com
strict-ssl=true               # set to false ONLY for known self-signed certs

# Workspaces
include-workspace-root=true   # apply scripts to root too
workspaces-update=true

Inspect current effective config:

bash
npm config list

Output:

text
; "user" config from /home/alice/.npmrc
registry = "https://registry.npmjs.org/"
save-exact = true

; "project" config from /home/alice/my-app/.npmrc
@my-org:registry = "https://npm.pkg.github.com/"
engine-strict = true

; node bin location = /home/alice/.nvm/versions/node/v22.14.0/bin/node
; node version = v22.14.0
; npm local prefix = /home/alice/my-app
; npm version = 10.9.2

Get a single value (useful in scripts):

bash
npm config get registry

Output:

text
https://registry.npmjs.org/

Publishing — full lifecycle

Publishing to the npm registry takes some preparation. The basic flow: log in, version-bump, verify with a dry-run pack, publish, push the git tag.

Pre-publish checklist

json
{
  "name": "@my-org/awesome-lib",
  "version": "1.0.0",
  "description": "An awesome library",
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist", "README.md", "LICENSE"],
  "sideEffects": false,
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/",
    "provenance": true
  },
  "engines": { "node": ">=20" },
  "repository": { "type": "git", "url": "git+https://github.com/my-org/awesome-lib.git" }
}

Key fields:

  • files — allowlist; only listed paths ship. Without it, npm uses .npmignore (or .gitignore) — easy to leak source files by accident.
  • exports — modern entry-point map. Replaces main. Allows dual ESM/CJS, conditional exports, subpath imports.
  • sideEffects — bundlers (webpack, Vite) tree-shake aggressively when this is false.
  • publishConfig.accesspublic for scoped packages (default is restricted).
  • publishConfig.provenance — emits a signed attestation linking the publish to your CI run. Highly recommended for trust.

Dry-run before publishing

npm pack --dry-run shows exactly what would ship. Always run this once before your first publish.

bash
npm pack --dry-run

Output:

text
npm notice 📦  @my-org/awesome-lib@1.0.0
npm notice === Tarball Contents ===
npm notice 1.4kB README.md
npm notice 5.2kB dist/index.cjs
npm notice 4.8kB dist/index.mjs
npm notice 1.1kB dist/index.d.ts
npm notice 0.7kB LICENSE
npm notice 0.4kB package.json
npm notice === Tarball Details ===
npm notice name:          @my-org/awesome-lib
npm notice version:       1.0.0
npm notice package size:  4.3 kB
npm notice unpacked size: 13.6 kB
npm notice total files:   6

Publishing with provenance (signed releases)

Provenance attaches a signed certificate proving the package was built by a specific CI workflow on a specific commit. The npm CLI requires the build to run inside a supported CI (GitHub Actions today, GitLab CI tomorrow). It's free and dramatically improves supply-chain trust.

bash
# In CI (GitHub Actions), with id-token: write permission
npm publish --provenance --access public

Output:

text
npm notice publishing with provenance
npm notice Generated provenance statement: https://search.sigstore.dev/?logIndex=...
+ @my-org/awesome-lib@1.0.0

Distribution tags

dist-tags point readable names at versions. latest is the default tag; next, beta, and experimental are common alternatives.

bash
# Publish a beta release without making it the default
npm publish --tag beta

# Move the "latest" pointer to an older version (rollback)
npm dist-tag add @my-org/awesome-lib@1.0.5 latest

# List dist-tags for a package
npm dist-tag ls @my-org/awesome-lib

Output (npm dist-tag ls):

text
beta: 2.0.0-beta.3
latest: 1.0.5
next: 1.0.6

Unpublishing and deprecation

Unpublishing is restricted: only within 72 hours of publish, and only if no other package depends on yours. Prefer deprecate:

bash
# Deprecate a single version
npm deprecate @my-org/awesome-lib@1.0.0 "Critical bug — upgrade to 1.0.1"

# Deprecate a range
npm deprecate "@my-org/awesome-lib@<1.0.5" "Security issue — upgrade to >=1.0.5"

# Unpublish a single version (within 72 hours)
npm unpublish @my-org/awesome-lib@1.0.0

# Unpublish the whole package (within 72 hours)
npm unpublish @my-org/awesome-lib --force

Output: (none — exits 0 on success)

Two-factor authentication

2FA is required on the npm registry for publishing since 2022. Configure it for both your account and your packages.

bash
# Enable 2FA on the account (interactive — scan QR code with authenticator app)
npm profile enable-2fa auth-and-writes

# Require 2FA tokens on every publish/dist-tag/owner-change for a package
npm access 2fa-required @my-org/awesome-lib

Output: (none — exits 0 on success)

In CI, use a granular access token instead of your interactive password — generated at <https://www.npmjs.com/settings//tokens/granular-access-tokens>.

Workspaces — deep-dive

npm has supported workspaces since v7. The model is simpler than pnpm/Yarn — workspaces are just nested packages that share the root node_modules.

Setup

json
// root package.json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*",
    "tools/cli"
  ]
}

Targeting workspaces

bash
# Run a script in a specific workspace
npm run build -w @my-org/ui
npm run build --workspace=packages/ui

# Run across all workspaces
npm run test --workspaces

# Skip workspaces without the script
npm run lint --workspaces --if-present

# Add a dep to a specific workspace
npm install lodash -w apps/web

# Add a workspace dep (the package must exist in the workspace)
npm install @my-org/ui -w apps/web

# Add a dep to multiple workspaces at once
npm install vitest -D -w @my-org/ui -w @my-org/api

Output (npm run test --workspaces):

text
> @my-org/ui@1.0.0 test
> vitest run
 ✓ Button.test.tsx (3 tests)

> @my-org/api@1.0.0 test
> vitest run
 ✓ routes.test.ts (8 tests)

Workspace-aware install caching

npm ci --workspaces installs everything in a deterministic order. Useful in CI:

bash
npm ci --workspaces --include-workspace-root

Output: (none — exits 0 on success)

npm vs pnpm vs Yarn vs Bun — comparison

Featurenpm v10pnpm v9Yarn v4Bun v1.2
Ships withNode.jsStandaloneCorepackStandalone
Lockfilepackage-lock.jsonpnpm-lock.yamlyarn.lockbun.lock
Install modelFlat hoistedSymlinked storePnP / hoistedFlat hoisted
Cold install (300 pkgs)~45 s baseline~18 s~25 s~3 s
Disk per project~250 MB (copies)~5 MB (links)~20 MB (PnP zip)~250 MB
WorkspacesYesYes + catalogsYes + constraintsYes
Native TS execNoNoVia tsxYes
Built-in registrynpmnpmnpmnpm
Provenance signingYesVia npmVia npmNo
npm audit integrationBuilt-inpnpm audityarn npm auditLimited
Ecosystem maturityMost stableStableStableMaturing

Pick npm when zero-setup matters and you're not on a monorepo with disk/install pain. Pick pnpm for monorepos and disk efficiency. Pick yarn for Plug'n'Play and constraints. Pick bun when speed dominates.

Common pitfalls

  1. npm install not respecting the lockfilenpm install reconciles package.json with the lockfile; if they disagree, it updates the lockfile. Use npm ci for read-only install or npm install --package-lock-only to update lockfile only.
  2. node_modules corruption after a manual deleterm -rf node_modules && npm install is safe; partial deletes can leave broken symlinks. Always nuke the whole directory.
  3. postinstall running malicious code — supply-chain attacks. Audit new deps with npm view <pkg> and run --ignore-scripts for first install.
  4. EACCES on global install — never sudo npm install -g. Set prefix=~/.npm-global in .npmrc and add ~/.npm-global/bin to PATH. Or use a Node version manager — see installation.
  5. engine-strict=true blocking install — a package's engines.node doesn't match. Update Node or the package. Do not disable engine-strict without auditing.
  6. Peer dep "ERESOLVE" loops--legacy-peer-deps (npm 7+) reverts to npm 6 behaviour of installing without resolution. Use sparingly; better to fix the version mismatch.
  7. npm publish shipping junk.npmignore or files controls the tarball. Always run npm pack --dry-run once before the first publish.
  8. Lockfile churn in CI — different Node versions can produce different lockfiles. Pin Node version (engines.node + .nvmrc) and rerun npm install after upgrading.
  9. @scope 404 from a private registry — missing scope route in .npmrc. Add @scope:registry=<url> and a token line. Verify with npm whoami --registry=<url>.
  10. npm audit exiting non-zero in CI — set audit-level=high in .npmrc to only fail on actually serious findings, not every "low" advisory.

Real-world recipes

Reproducible Docker builds with npm ci

The canonical Dockerfile snippet — copy lockfile + package.json first so layer caches stay valid across code changes.

dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force

FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["node", "dist/server.js"]
bash
docker build -t my-app .

Output:

text
[+] Building 28.4s (12/12) FINISHED
 => exporting to image  0.3s
 => => writing image sha256:9f8e7d6c5b4a3210
 => => naming to docker.io/library/my-app

Upgrade all packages with confidence

npm-check-updates (run via npx) bumps version ranges in package.json to their latest, then a regular npm install updates the lockfile.

bash
# Preview the upgrade
npx npm-check-updates

# Apply
npx npm-check-updates -u
npm install
npm test

Output:

text
 express              ^4.18.2  →  ^4.21.0
 typescript           ^5.3.3   →  ^5.5.4
 vitest               ^1.6.0   →  ^2.0.5

Run "ncu -u" to upgrade package.json

Auto-publish on git tag (GitHub Actions)

Publish only when a v* tag is pushed. With npm publish --provenance, every release gets a signed attestation.

yaml
# .github/workflows/release.yml
on:
  push:
    tags: ['v*']

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm run build
      - run: npm test
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
bash
git tag v1.2.0
git push --follow-tags

Output: (none — exits 0 on success)

Cache npm install in CI

GitHub Actions' setup-node ships built-in caching. Key on package-lock.json; cold runs install fresh, warm runs restore from cache.

yaml
- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: npm
- run: npm ci
bash
npm ci

Output:

text
added 247 packages in 4s

Force a specific transitive dependency version

overrides (npm 8.3+) is npm's answer to Yarn's resolutions. It pins a transitive dependency to a specific version everywhere it appears in the tree.

json
{
  "overrides": {
    "minimist": "^1.2.8",
    "follow-redirects": "^1.15.6",
    "axios": {
      "follow-redirects": "^1.15.6"
    }
  }
}
bash
npm install
npm why minimist

Output:

text
minimist@1.2.8
node_modules/minimist
  minimist@"^1.2.8" from the root project
  minimist@"^1.2.0" from mocha@10.7.0 (overridden)

Set up a private @scope registry

Authenticate against a private registry (GitHub Packages, GitLab, Verdaccio) for a single scope while leaving public packages on npmjs.org.

ini
# .npmrc (project)
@my-org:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
always-auth=true
bash
export GITHUB_TOKEN="ghp_xxx"
npm install @my-org/private-lib

Output: (none — exits 0 on success)

Migrate a project from yarn to npm

Re-resolve from package.json, generate package-lock.json, delete Yarn artifacts.

bash
rm -rf node_modules yarn.lock .yarn .yarnrc.yml .pnp.cjs .pnp.loader.mjs
npm install
git add package-lock.json
git rm yarn.lock .yarnrc.yml -r --cached .yarn .pnp.cjs .pnp.loader.mjs 2>/dev/null || true

Output: (none — exits 0 on success)

Find and remove unused dependencies

depcheck (via npx) scans your source for import/require calls and reports declared-but-unused dependencies.

bash
npx depcheck

Output:

text
Unused dependencies
* lodash
* moment

Unused devDependencies
* @types/node

Then remove them:

bash
npm uninstall lodash moment @types/node

Output: (none — exits 0 on success)