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_modules folder 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:

  1. You can’t build modern webapps without it. Every framework, every tutorial, every starter assumes npm. The first command after git clone is npm install.

  2. Lockfile discipline = reproducible builds. Without lockfiles, two installs of the same package.json can produce different node_modules (security patches, semver drift). Lockfiles freeze the exact version tree so your laptop, CI, and Vercel all install identical code.

  3. Scripts are the entrypoint to everything. npm run dev, npm run build, npm test — these one-liners hide the complex commands underneath. Every project’s package.json scripts block 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 registry
  • scripts — named commands you can run with npm run <name>
  • dependencies — packages your APP needs at runtime
  • devDependencies — 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:

SymbolMeaning^15.0.0 allows
^ (caret)Same major version15.0.0 → 15.999.999
~ (tilde)Same minor version15.0.0 → 15.0.999
(none)Exact versiononly 15.0.0
*Any versiondangerous; 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 first

For 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

ToolSpeedDisk usageLockfileWhen it matters
npmBaselineBaseline (separate node_modules per project)package-lock.jsonSafest default. Ships with Node. Universally supported.
pnpm2-3Ă— faster than npmDramatically less (uses content-addressable global store + symlinks)pnpm-lock.yamlGreat choice when you have many projects sharing common dependencies. Vercel, Next.js, many monorepos use it.
yarn1.5-2× faster than npm classic; v4+ is fastComparable to npmyarn.lockOlder choice, still widely used. Yarn v4 (“Berry”) is a separate tool with different conventions.
bun10× faster on installComparable to npmbun.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* and post* hooks — npm run build automatically runs prebuild first and postbuild after. 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 -rf and other Unix commands fail on Windows. Use the rimraf package or cross-platform alternatives. In 2026, more projects use npm exec or npx to 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 install

npx 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.json to “fix” install problems. It silently changes your dependency tree. Use npm ci (clean install) instead — it’s faster anyway.

  • npm install vs npm ci — both install dependencies, but:

    • npm install may MODIFY the lockfile to add new entries
    • npm ci requires an existing lockfile, installs exactly what’s in it, fails on mismatch
    • Use npm ci in CI. Faster, reproducible, won’t surprise you.
  • Mixed lockfiles in one repo = chaos. package-lock.json from npm + pnpm-lock.yaml from pnpm = two truths. Delete the one you’re not using; commit only one.

  • node_modules size 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-canvas ship 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.

  • peerDependencies are 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.0 allows minor upgrades, but the lockfile pins the exact version. To actually get a minor upgrade, you need npm update (lockfile-aware) or npm 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 audit is noisy. It reports vulnerabilities in dev dependencies, dependencies-of-dependencies, etc. Most aren’t exploitable in your context. Read carefully before running npm audit fix --force — it can upgrade things you don’t want upgraded.

  • npm audit fix can break the build. It happily moves packages to versions outside your declared range. Use --dry-run first 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. Prefer npx <pkg> for tools you run occasionally.

  • engines is a soft constraint by default. A Node-22-only package will still INSTALL on Node 18 unless engine-strict=true is set. Vercel reads engines.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.

  • .npmrc per-project changes behavior. A .npmrc file 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 .npmrc with an _authToken= line is a credential. Never commit one to git.

  • Some packages have install scripts that run arbitrary code. Every npm install runs postinstall scripts for installed packages. This has been exploited (e.g., the colors saga in 2022). The --ignore-scripts flag exists; for high-trust environments, audit before installing fresh dependencies.

  • npx <name> runs the LATEST version unless pinned. npx create-next-app@latest is explicit; npx create-next-app may run a stale local version. When in doubt, pin.

  • Don’t put binaries in dependencies for libraries. A library published to npm should NOT depend on dev-only tools. Use devDependencies. Users of your library should NEVER need to install ESLint, prettier, vitest because they imported your code.

  • private: true is critical for app projects. Prevents npm publish from accidentally pushing your private webapp source to the npm registry.

  • Versions with 0.x mean “no compatibility guarantees.” 0.5.0 → 0.6.0 may be a breaking change by convention. Once a package hits 1.0.0, semver kicks in formally.

  • NPM cache lives in ~/.npm. Occasionally gets corrupted. npm cache clean --force fixes it. Rare.

  • CI rebuilds are slow if you don’t cache ~/.npm or node_modules. GitHub Actions’ actions/setup-node@v4 has cache: '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, use patch-package.

  • npm install vs npm i are identical. i is just the short alias.

  • npm updates itself. npm install -g npm@latest upgrades npm itself. The version is independent of Node.

  • For TypeScript packages, @types/* matters. A JavaScript library may need a separate @types/library-name package 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-package and you accept. Later you wonder why the project has 50 packages. Periodically npm list --depth=0 and audit what’s there.

  • The packageManager field 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


Sources