npm & package managers
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: The tool that downloads and tracks every third-party library your project depends on — and the file (
package.json) that records what you depend on, what scripts you can run, and exactly which versions are installed.
In plain English
Modern web development is built on standing on other people’s code. A typical Next.js project starts with maybe 5-10 lines of YOUR code and ~50,000 lines of OTHER PEOPLE’S code (React, Tailwind, TypeScript, Next.js itself, hundreds of utility libraries). That code lives in packages — bundles of JavaScript published to a central registry, downloadable by name.
A package manager is the program that handles this:
- Reads your project’s manifest of “I need these libraries at these versions”
- Downloads them from the registry
- Resolves their dependencies (the libraries THEY need), recursively
- Stores them in a
node_modulesfolder inside your project - Records exactly what got installed in a lockfile (so the same install reproduces later)
- Adds/removes/updates packages on command
- Runs scripts defined in your
package.json
npm (Node Package Manager) is the original — it ships with Node.js itself. pnpm, yarn, and bun are alternatives. They differ in speed, disk-usage efficiency, and exact behavior, but for everyday work they’re nearly interchangeable.
In 2026, all four are mature production tools. For new projects: npm is the safest default; pnpm is the slick choice if you want speed + disk efficiency.
This entry is about USING these tools day-to-day. For Node.js itself (the runtime that runs these tools), see Node.js (runtime) and Node.js (concept).
Why it matters
Three reasons package management is foundational:
-
You can’t build modern webapps without it. Every framework, every tutorial, every starter assumes npm. The first command after
git cloneisnpm install. -
Lockfile discipline = reproducible builds. Without lockfiles, two installs of the same
package.jsoncan produce differentnode_modules(security patches, semver drift). Lockfiles freeze the exact version tree so your laptop, CI, and Vercel all install identical code. -
Scripts are the entrypoint to everything.
npm run dev,npm run build,npm test— these one-liners hide the complex commands underneath. Every project’spackage.json scriptsblock IS its build documentation.
The trade-off: the npm ecosystem is huge, fast-moving, and occasionally hostile. Security incidents happen. Packages get unpublished. Dependencies of dependencies get yanked. Lockfiles and auditing exist because the ecosystem isn’t 100% trustworthy.
The two key files
package.json — what you depend on
Sits at the root of your project. A typical Next.js project’s package.json:
{
"name": "stmarkbible",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"typescript": "^5.4.0",
"eslint": "^9.0.0"
},
"engines": {
"node": ">=20"
}
}Key fields:
name— your project name (lowercased, kebab-case)version— your project’s version (semver:0.1.0)private: true— prevents accidentally publishing this to the npm registryscripts— named commands you can run withnpm run <name>dependencies— packages your APP needs at runtimedevDependencies— packages only needed during development (testing, linting, building)engines— declares which Node versions you support; Vercel and other hosts respect this
package-lock.json — exactly what’s installed
Auto-generated by npm. Contains the exact resolved version of every package and every nested dependency, plus integrity hashes for tamper detection.
Always commit package-lock.json to git. It’s how teammates, CI, and Vercel install identical trees. Deleting it breaks reproducible builds.
If you use pnpm instead: pnpm-lock.yaml. If yarn: yarn.lock. If bun: bun.lockb (binary). One lockfile per project — never mix package managers in the same repo.
The semver caret (^) explained
You’ll see version ranges like:
"next": "^15.0.0"Semver (Semantic Versioning) is the convention: MAJOR.MINOR.PATCH.
- Major (15): breaking changes
- Minor (15.1): new features, backwards-compatible
- Patch (15.1.2): bug fixes only
The leading character defines what UPGRADES are allowed when you run npm update:
| Symbol | Meaning | ^15.0.0 allows |
|---|---|---|
^ (caret) | Same major version | 15.0.0 → 15.999.999 |
~ (tilde) | Same minor version | 15.0.0 → 15.0.999 |
| (none) | Exact version | only 15.0.0 |
* | Any version | dangerous; don’t use |
The lockfile pins the EXACT installed version anyway, so the caret matters mainly for npm install <pkg>@latest decisions and npm update behavior.
The essential commands
# Install all dependencies from package.json (after cloning)
npm install # or: npm i
# Install with strict lockfile (use in CI)
npm ci # faster, requires lockfile, deletes node_modules first
# Add a runtime dependency
npm install react
# Add a dev-only dependency
npm install -D typescript # or: --save-dev
# Add a specific version
npm install react@19.0.0
# Add globally (rare; usually avoid)
npm install -g vercel
# Remove a dependency
npm uninstall react
# Update all dependencies (respecting semver ranges)
npm update
# Update one to the latest matching version
npm update react
# Update to the latest (ignoring semver range)
npm install react@latest
# Run a script defined in package.json
npm run dev
npm run build
npm test # `test` is a magic name; npm test works without "run"
npm start # same — `start` is magic
# Check what's installed at the top level
npm list
# Check what's outdated
npm outdated
# Audit for known vulnerabilities
npm audit
npm audit fix # tries to auto-fix; review changes firstFor everyday work, the seven commands you’ll use most are install, ci, i -D, uninstall, update, run, audit.
A concrete example: starting a new project
# 1. Create the project
npx create-next-app@latest my-app
cd my-app
# create-next-app already ran npm install for you.
# package.json, package-lock.json, node_modules/ all exist.
# 2. Add a runtime dependency
npm install zod
# 3. Add a dev-only dependency
npm install -D vitest @vitest/ui
# 4. Add a script to package.json
# (edit by hand)
# "scripts": {
# ...
# "test": "vitest"
# }
# 5. Run scripts
npm run dev # starts Next.js dev server
npm test # runs vitest
# 6. Commit everything (including package-lock.json)
git add package.json package-lock.json
git commit -m "Add zod, vitest"What’s NOT in git: the node_modules folder. It’s enormous (500MB+ is normal), reproducible from the lockfile, and never committed. The .gitignore from create-next-app already excludes it.
The four package managers compared
| Tool | Speed | Disk usage | Lockfile | When it matters |
|---|---|---|---|---|
| npm | Baseline | Baseline (separate node_modules per project) | package-lock.json | Safest default. Ships with Node. Universally supported. |
| pnpm | 2-3Ă— faster than npm | Dramatically less (uses content-addressable global store + symlinks) | pnpm-lock.yaml | Great choice when you have many projects sharing common dependencies. Vercel, Next.js, many monorepos use it. |
| yarn | 1.5-2× faster than npm classic; v4+ is fast | Comparable to npm | yarn.lock | Older choice, still widely used. Yarn v4 (“Berry”) is a separate tool with different conventions. |
| bun | 10× faster on install | Comparable to npm | bun.lockb (binary) | Newest; very fast; also a runtime (alternative to Node). Use if you’re already in the Bun world. |
For solo projects in 2026: npm or pnpm. Both work; pick one per project; never mix.
For Bible Quest-style projects: the playbook defaults to npm because it’s universal and well-supported by every tutorial. Switching to pnpm is fine but introduces unfamiliar lockfiles for collaborators.
Scripts — the unsung hero of package.json
The "scripts" block is where projects define how to run themselves:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:e2e": "playwright test",
"format": "prettier --write .",
"check": "npm run lint && npm run type-check && npm run build",
"predeploy": "npm run check && npm test"
}
}A few patterns worth knowing:
pre*andpost*hooks —npm run buildautomatically runsprebuildfirst andpostbuildafter. Useful for prep / cleanup. (Less common in 2026; explicit chained scripts are clearer.)&&for sequential —"check": "lint && tsc && build"runs each in order, stops on first failure.||for fallbacks —"build": "next build || echo failed"(rare).- NPM
_scripts — names starting with_are conventional for “internal helpers not meant to be run directly.” - Cross-platform pain —
rm -rfand other Unix commands fail on Windows. Use therimrafpackage or cross-platform alternatives. In 2026, more projects usenpm execornpxto avoid this.
npx and npm exec — running tools without installing globally
You’ll see this constantly:
npx create-next-app@latest my-app
npx shadcn@latest add button
npx prettier --write .
npx playwright installnpx runs a package’s binary as a one-off — downloads it if not present, runs it, optionally removes it. No global install required.
This is THE preferred way to run tool-style npm packages. Avoids cluttering the global Node install, makes commands reproducible across machines, and pins versions clearly.
npm exec is the more recent equivalent (same effect, slightly different ergonomics).
Workspaces — for monorepos
If you have multiple related packages in one repo (a typical monorepo), all four tools support workspaces:
{
"name": "monorepo-root",
"workspaces": [
"apps/*",
"packages/*"
]
}Now npm install from the root installs ALL packages’ dependencies into one shared node_modules, with smart resolution. You can have an apps/web Next.js app + a packages/shared utility library, with the app importing from the library without publishing.
For typical solo projects, you don’t need workspaces. They become useful when you have 2+ apps sharing real code.
Common gotchas
-
Never delete
package-lock.jsonto “fix” install problems. It silently changes your dependency tree. Usenpm ci(clean install) instead — it’s faster anyway. -
npm installvsnpm ci— both install dependencies, but:npm installmay MODIFY the lockfile to add new entriesnpm cirequires an existing lockfile, installs exactly what’s in it, fails on mismatch- Use
npm ciin CI. Faster, reproducible, won’t surprise you.
-
Mixed lockfiles in one repo = chaos.
package-lock.jsonfrom npm +pnpm-lock.yamlfrom pnpm = two truths. Delete the one you’re not using; commit only one. -
node_modulessize shocks people. 500MB-2GB is normal. Don’t try to optimize it — bundlers strip it down for production. -
Some packages have heavy native binaries.
sharp(image processing),puppeteer,playwright,node-canvasship platform-specific binaries. A Mac install doesn’t work on Linux without re-fetching. Vercel handles this in CI; rolling your own deploys might not. -
peerDependenciesare weird. A package can declare “I work with React 18+, but I won’t install it myself — you must.” Modern package managers warn or auto-install peer deps; older setups need manual handling. -
Caret (
^) ranges + lockfiles can drift.^15.0.0allows minor upgrades, but the lockfile pins the exact version. To actually get a minor upgrade, you neednpm update(lockfile-aware) ornpm install <pkg>@latest(force update). -
A package can be unpublished. Famously, the 2016 left-pad incident. Modern npm puts limits on unpublishing, but pinned versions can still vanish. Audit critical dependencies.
-
npm auditis noisy. It reports vulnerabilities in dev dependencies, dependencies-of-dependencies, etc. Most aren’t exploitable in your context. Read carefully before runningnpm audit fix --force— it can upgrade things you don’t want upgraded. -
npm audit fixcan break the build. It happily moves packages to versions outside your declared range. Use--dry-runfirst to see what it’d change. -
Don’t install packages globally if you don’t have to.
npm install -g <pkg>pollutes a single shared spot. Prefernpx <pkg>for tools you run occasionally. -
enginesis a soft constraint by default. A Node-22-only package will still INSTALL on Node 18 unlessengine-strict=trueis set. Vercel readsengines.node; locally it’s a warning. -
Lockfile merge conflicts in PRs are painful. When two PRs add dependencies, git can’t always auto-merge. Delete the lockfile, re-run
npm install, commit. Or use a tool that knows lockfiles. -
.npmrcper-project changes behavior. A.npmrcfile in the project root can pin registry URLs, auth tokens, install behavior. Useful for private registries; surprising if you didn’t write it. -
NPM tokens are sensitive. A
.npmrcwith an_authToken=line is a credential. Never commit one to git. -
Some packages have install scripts that run arbitrary code. Every
npm installrunspostinstallscripts for installed packages. This has been exploited (e.g., thecolorssaga in 2022). The--ignore-scriptsflag exists; for high-trust environments, audit before installing fresh dependencies. -
npx <name>runs the LATEST version unless pinned.npx create-next-app@latestis explicit;npx create-next-appmay run a stale local version. When in doubt, pin. -
Don’t put binaries in
dependenciesfor libraries. A library published to npm should NOT depend on dev-only tools. UsedevDependencies. Users of your library should NEVER need to install ESLint, prettier, vitest because they imported your code. -
private: trueis critical for app projects. Preventsnpm publishfrom accidentally pushing your private webapp source to the npm registry. -
Versions with
0.xmean “no compatibility guarantees.”0.5.0→0.6.0may be a breaking change by convention. Once a package hits1.0.0, semver kicks in formally. -
NPM cache lives in
~/.npm. Occasionally gets corrupted.npm cache clean --forcefixes it. Rare. -
CI rebuilds are slow if you don’t cache
~/.npmornode_modules. GitHub Actions’actions/setup-node@v4hascache: 'npm'— use it to cut install time from 60s to 5s on subsequent runs. -
Don’t edit
node_modules. It’s regenerated on every install. Edits are lost. If you need to patch a dependency, usepatch-package. -
npm installvsnpm iare identical.iis just the short alias. -
npmupdates itself.npm install -g npm@latestupgrades npm itself. The version is independent of Node. -
For TypeScript packages,
@types/*matters. A JavaScript library may need a separate@types/library-namepackage for TypeScript types.@types/node,@types/react,@types/express. Modern libraries ship their own types; check the package’s README. -
AI tools may add packages without you realizing. Claude or another AI may suggest
npm install some-packageand you accept. Later you wonder why the project has 50 packages. Periodicallynpm list --depth=0and audit what’s there. -
The
packageManagerfield in package.json locks the package manager + version. Corepack (built into Node 16+) enforces it."packageManager": "npm@10.8.0"means “this project uses npm 10.8.0; anything else errors.” Useful for monorepos with strong opinions. -
GitHub Dependabot can spam you with upgrade PRs. Set sensible defaults: weekly, group minor/patch, ignore major bumps you don’t want.
When to consider switching to pnpm or bun
Defaults: npm.
Switch to pnpm if:
- You have multiple projects on your machine and want to save disk space
- You’re working in a monorepo (pnpm’s workspace handling is excellent)
- You’re working on a project that already uses pnpm
- Install speed matters (CI runs frequently)
Switch to bun if:
- You’re already using Bun as your runtime (it replaces Node + npm in one tool)
- Install speed is critical and you’ve ruled out other reasons
Don’t switch to yarn in 2026 unless the project already uses it. The yarn-vs-npm advantage that mattered in 2018 has narrowed.
See also
- Node.js (runtime — practical tooling view) 🟥 — installing Node, version management
- Node.js (concept) 🟩 🟦 — what Node actually is
- The terminal & command line 🟩 — where you type npm commands
- VS Code 🟩 — runs
npm installfor you via “Run Task” - Windows dev environment 🟩 — Windows-specific npm quirks
- PowerShell vs Bash 🟥 — script syntax differences
- Environment variables 🟩 — many npm packages read
process.env - CD 🟩 — where
npm ciruns - JavaScript 🟩 — what npm packages are written in
- Run the production build locally 🟩 — uses npm scripts
- Glossary: npm, node_modules, semver, Lockfile
Sources
- npm docs — canonical reference
- pnpm docs
- Yarn docs
- Bun docs
- semver.org — the versioning spec
- npm best practices guide — official scripts reference
- npm Security — audit, signed publishing