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:
npm --version
Output:
10.9.2
Upgrade npm itself to the latest version:
npm install -g npm@latest
Output: (none — exits 0 on success)
Initialize a project
# Interactive prompts
npm init
# Skip all prompts and accept defaults
npm init -y
Output (npm init -y):
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
# 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
npm uninstall express
npm uninstall --save-dev eslint
npm uninstall -g typescript
Output: (none — exits 0 on success)
package.json anatomy
{
"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
| Field | Installed when | Purpose |
|---|---|---|
dependencies | Always | Runtime — shipped to users |
devDependencies | Not in production (npm ci --omit=dev) | Build tools, linters, test frameworks |
peerDependencies | Must be provided by the consumer | Plugin / library host packages (e.g., React plugins) |
optionalDependencies | Install attempted but failure is ignored | Platform-specific native bindings |
Running scripts
# 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:
{
"scripts": {
"prebuild": "npm run lint",
"build": "tsc",
"postbuild": "echo Build complete"
}
}
Listing and inspecting packages
# 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):
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
npm outdated
Output:
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
# 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
# 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):
# 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
# 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)
Local development with npm link
Link a local package into another project without publishing to the registry:
# 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.
# 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:
# Install ignoring peer dependency errors (use cautiously)
npm install --legacy-peer-deps
Output: (none — exits 0 on success)
Workspaces (monorepos)
// Root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}
# 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):
> my-lib@1.0.0 build
> tsc
> my-app@0.1.0 build
> next build
✓ Compiled successfully
Registry configuration
# 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):
# .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
| Flag | Effect |
|---|---|
--dry-run | Show what would happen without doing it |
--prefer-offline | Use cached packages when possible |
--legacy-peer-deps | Ignore peer dependency conflicts |
--ignore-scripts | Skip lifecycle scripts (safer for untrusted packages) |
--omit=dev | Skip devDependencies (production install) |
--fund | Show funding info |
--verbose | Detailed logging |
--silent | Suppress all output |
--json | Output as JSON |
Common errors and fixes
| Error | Likely cause | Fix |
|---|---|---|
EACCES: permission denied | Global install without permission | Use nvm or add npm prefix to PATH instead of sudo |
ERESOLVE unable to resolve dependency tree | Peer dependency conflict | --legacy-peer-deps or update conflicting packages |
Cannot find module 'X' | Package not installed or missing from package.json | npm install X |
npm ERR! code ENOENT | Running npm in wrong directory or missing package.json | Check working directory |
npm warn deprecated | Package uses an old API | Check if a maintained replacement exists |
code EINTEGRITY | Corrupted cache or lockfile hash mismatch | npm cache clean --force && npm ci |
npm ERR! 404 Not Found | Package name typo or private package without auth | Check 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
{
"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" }
}
}
}
| Field | Meaning |
|---|---|
lockfileVersion | 1 (npm 5–6), 2 (npm 7+), 3 (npm 7+ optimised) |
packages | Tree keyed by location relative to project root |
resolved | The exact tarball URL fetched |
integrity | SHA-512 hash verified on every install |
requires | Legacy v1 compatibility flag |
Lockfile v3 is significantly smaller than v2 — same content, no
dependenciesblock at the top. Modern npm writes v3 by default. Convert by deleting the lockfile and runningnpm 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.
# 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):
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:
# 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.
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:
npm list --all --parseable | head -10
Output:
/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:
npm dedupe
Output:
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
| Event | When it fires | Typical use |
|---|---|---|
preinstall | Before deps are installed | Validate Node version, set env |
install / postinstall | After deps are installed | Native build (node-gyp), patch source |
prepare | Before pack/publish, after install | TypeScript compile, copy assets |
prepack / postpack | Around tarball generation | Modify files for publish |
prepublishOnly | Before publish (not on install) | Final checks, build, tests |
prepublish (deprecated) | Before publish + before install | Avoid — confusing semantics |
publish / postpublish | After publish completes | Notify, tag, push docs |
preversion / postversion | Around npm version | Run tests, push tag |
pre<name> / post<name> | Around any custom script | Generic hook |
{
"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:
# 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-scriptsis a security best practice for global tools you don't trust yet. After installing, audit the package and re-runnpm rebuildto execute scripts deliberately.
.npmrc configuration reference
.npmrc is npm's config file. npm reads from four locations in order — later wins:
~/.npmrc— per-user<prefix>/etc/npmrc— global (wherenpm prefix -gpoints)./.npmrc— per-project- Environment variables (
NPM_CONFIG_<KEY>=<value>)
# .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:
npm config list
Output:
; "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):
npm config get registry
Output:
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
{
"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. Replacesmain. Allows dual ESM/CJS, conditional exports, subpath imports.sideEffects— bundlers (webpack, Vite) tree-shake aggressively when this isfalse.publishConfig.access—publicfor scoped packages (default isrestricted).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.
npm pack --dry-run
Output:
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.
# In CI (GitHub Actions), with id-token: write permission
npm publish --provenance --access public
Output:
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.
# 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):
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:
# 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.
# 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
// root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*",
"tools/cli"
]
}
Targeting workspaces
# 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):
> @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:
npm ci --workspaces --include-workspace-root
Output: (none — exits 0 on success)
npm vs pnpm vs Yarn vs Bun — comparison
| Feature | npm v10 | pnpm v9 | Yarn v4 | Bun v1.2 |
|---|---|---|---|---|
| Ships with | Node.js | Standalone | Corepack | Standalone |
| Lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock | bun.lock |
| Install model | Flat hoisted | Symlinked store | PnP / hoisted | Flat 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 |
| Workspaces | Yes | Yes + catalogs | Yes + constraints | Yes |
| Native TS exec | No | No | Via tsx | Yes |
| Built-in registry | npm | npm | npm | npm |
| Provenance signing | Yes | Via npm | Via npm | No |
npm audit integration | Built-in | pnpm audit | yarn npm audit | Limited |
| Ecosystem maturity | Most stable | Stable | Stable | Maturing |
Pick npm when zero-setup matters and you're not on a monorepo with disk/install pain. Pick
pnpmfor monorepos and disk efficiency. Pickyarnfor Plug'n'Play and constraints. Pickbunwhen speed dominates.
Common pitfalls
npm installnot respecting the lockfile —npm installreconcilespackage.jsonwith the lockfile; if they disagree, it updates the lockfile. Usenpm cifor read-only install ornpm install --package-lock-onlyto update lockfile only.node_modulescorruption after a manual delete —rm -rf node_modules && npm installis safe; partial deletes can leave broken symlinks. Always nuke the whole directory.postinstallrunning malicious code — supply-chain attacks. Audit new deps withnpm view <pkg>and run--ignore-scriptsfor first install.- EACCES on global install — never
sudo npm install -g. Setprefix=~/.npm-globalin.npmrcand add~/.npm-global/bintoPATH. Or use a Node version manager — seeinstallation. engine-strict=trueblocking install — a package'sengines.nodedoesn't match. Update Node or the package. Do not disable engine-strict without auditing.- 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. npm publishshipping junk —.npmignoreorfilescontrols the tarball. Always runnpm pack --dry-runonce before the first publish.- Lockfile churn in CI — different Node versions can produce different lockfiles. Pin Node version (
engines.node+.nvmrc) and rerunnpm installafter upgrading. @scope404 from a private registry — missing scope route in.npmrc. Add@scope:registry=<url>and a token line. Verify withnpm whoami --registry=<url>.npm auditexiting non-zero in CI — setaudit-level=highin.npmrcto 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.
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"]
docker build -t my-app .
Output:
[+] 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.
# Preview the upgrade
npx npm-check-updates
# Apply
npx npm-check-updates -u
npm install
npm test
Output:
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.
# .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 }}
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.
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
npm ci
Output:
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.
{
"overrides": {
"minimist": "^1.2.8",
"follow-redirects": "^1.15.6",
"axios": {
"follow-redirects": "^1.15.6"
}
}
}
npm install
npm why minimist
Output:
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.
# .npmrc (project)
@my-org:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
always-auth=true
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.
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.
npx depcheck
Output:
Unused dependencies
* lodash
* moment
Unused devDependencies
* @types/node
Then remove them:
npm uninstall lodash moment @types/node
Output: (none — exits 0 on success)