npm Basics
npm is Node’s default package manager: install dependencies, run scripts, publish your own packages. Knowing the difference between install vs ci, dev vs prod deps, version ranges, and how the lockfile fits in covers 95% of daily use.
install/ci, scripts, lockfiles, versions
EXAMPLE
# 1) Anatomy of a package.json
{
"name": "my-app",
"version": "1.2.3",
"type": "module",
"main": "src/index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "node dist/server.js",
"test": "vitest",
"lint": "eslint .",
"prepare": "husky install"
},
"dependencies": {
"express": "^4.21.0",
"zod": "^3.23.0"
},
"devDependencies": {
"vitest": "^2.0.0",
"eslint": "^9.0.0",
"typescript": "^5.5.0"
},
"engines": { "node": ">=20" },
"private": true
}
# 2) Install
npm install # install everything from package.json + create/update lockfile
npm install express # add to dependencies (latest matching ^range)
npm install -D vitest # add to devDependencies
npm install -g pnpm # global (use sparingly — prefer npx)
npm install express@4.21.2 # exact version
npm install express@latest # update to latest
npm install gh:user/repo # install from a GitHub repo
# 3) npm ci — the CI / reproducible install
npm ci # clean install from lockfile; deletes node_modules first
# - 2-5x faster than 'install' in CI
# - REFUSES to run if package.json and lockfile disagree
# - ALWAYS prefer over 'npm install' in CI scripts
# 4) Dependencies vs devDependencies vs peerDependencies
# dependencies — runtime requirements (express, zod)
# devDependencies — build/test only (vitest, eslint, typescript)
# peerDependencies — your package expects the host to provide these (libraries you ship)
# optionalDependencies — install if possible, ignore failure (native bindings)
# In production, install only what's needed:
NODE_ENV=production npm ci --omit=dev
# OR
npm ci --omit=dev
# 5) Version ranges — semver
# ^1.2.3 → >=1.2.3 <2.0.0 (compatible patches + minors) ← npm default
# ~1.2.3 → >=1.2.3 <1.3.0 (compatible patches only)
# 1.2.3 → exactly 1.2.3 (no updates)
# 1.x → >=1.0.0 <2.0.0
# * → any version (avoid)
# >=1 <3 → manual range
# latest → resolves at install time (avoid in pinned deps)
# 6) Running scripts
npm run dev # runs scripts.dev
npm test # alias of 'npm run test' (also: start, restart, stop)
npm run build -- --mode=production # forward args after --
# Lifecycle scripts — auto-run
# preinstall, install, postinstall — risky for security; prefer 'prepare'
# prepare — runs after install; common for Husky hooks
# prepublishOnly — runs before publish; build artifacts here
# 7) npx — execute a binary without installing globally
npx vitest # runs the local binary
npx -p typescript tsc --init # one-shot from a registry package
npx create-react-app@latest my-app # the modern pattern
# 8) Inspecting
npm ls # tree of installed packages
npm ls --depth=0 # top-level only
npm outdated # what's newer than what you have
npm view express # registry info
npm view express versions --json # all published versions
npm explain react-dom # why is this package in my tree?
# 9) Updating safely
npm update # update to highest version matching the range in package.json
npm install express@latest # bumps the entry in package.json + lockfile
npx npm-check-updates -u # interactive 'bump every dependency to latest' tool
# 10) Audit
npm audit # vulnerability scan of installed tree
npm audit --omit=dev # production deps only
npm audit fix # auto-fix where possible (within current ranges)
npm audit fix --force # bump major versions (read the diff first)
# 11) Lockfile — package-lock.json
# - Records EXACT versions + integrity hashes of every package in the tree
# - ALWAYS commit it
# - Resolves the same tree everywhere (npm ci respects it strictly)
# - On merge conflicts: regenerate via 'npm install' and commit
# 12) Workspaces (monorepo)
# Root package.json
{
"workspaces": ["packages/*", "apps/*"],
"scripts": {
"build": "npm run build --workspaces --if-present"
}
}
# Install for all workspaces at once
npm install
# Add a dep to one workspace
npm install -w packages/utils lodash
# Run a script in one workspace
npm run -w apps/web dev
# 13) Publishing your own package
npm whoami # confirm login
npm version patch # 1.2.3 -> 1.2.4 (also creates a git tag)
npm publish # public package
npm publish --access=public # required for scoped packages on free accounts
npm publish --dry-run # preview what will be uploaded
# 14) .npmrc — registry + behaviour config
# project .npmrc (commit) — affects this project
registry=https://registry.npmjs.org/
save-exact=true # writes exact versions (no ^/~) when adding deps
fund=false # silence fund messages
audit=true
# user ~/.npmrc — tokens, never commit
//registry.npmjs.org/:_authToken=...
@my-org:registry=https://npm.my-org.com
# 15) Common bugs
# • Committing node_modules — wastes git, conflicts on merge — keep in .gitignore
# • 'npm install' in CI instead of 'npm ci' — slow + may resolve different deps than locked
# • Editing package.json by hand and forgetting to run 'npm install' — lockfile drifts
# • Mixing npm + yarn + pnpm in one repo — three lockfiles, three resolutions, chaos
# • Storing tokens in .npmrc committed to git — instant supply chain risk
# • Using 'latest' as a version constraint — non-reproducible builds
# • Forgetting --omit=dev in Docker COPY → ships TypeScript + ESLint to production
Why it matters
Commit your lockfile, run npm ci in CI, and split runtime vs build-time deps with --save-dev. The ^ in version ranges means “patch and minor updates” — safe by default but breakable; pin exact versions with save-exact=true for libraries you publish, leave ranges in apps where lockfiles do the pinning.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Exercise
Install a dev dependency with…
npm install
vitest
Two characters; flag for devDependency.
Discussion
Loading…