Package Management

Beyond npm install. Workspaces, monorepos, and professional dependency workflows.

Dependency Resolution

How package managers figure out what to install.

// When you install "express", it has its OWN dependencies:
// express → accepts, body-parser, cookie, ...
// body-parser → bytes, content-type, debug, ...
// Each of those has dependencies too!

// Your project installs 1 package → gets 50+ in node_modules
// This is the "dependency tree"

// Dependency types:
// Direct — packages YOU install (in your package.json)
// Transitive — dependencies OF your dependencies
// Peer — packages the library expects YOU to provide

// node_modules structure:
// node_modules/
// ├── express/           ← your dependency
// ├── accepts/           ← express's dependency (hoisted)
// ├── body-parser/       ← express's dependency (hoisted)
// └── .pnpm/             ← pnpm's content-addressable store

// Hoisting: npm/yarn flatten dependencies to top level
// This can cause "phantom dependencies" (using packages
// you didn't explicitly install)

// pnpm is strict: only YOUR dependencies are accessible
// at the top level. Transitive deps are hidden.
// This prevents accidental phantom dependency usage.

// Peer dependencies:
// "peerDependencies": { "react": "^18.0.0" }
// Means: "I work with React, but YOU must install it"
// Common in plugins and component libraries

Lock Files

Pin exact versions for reproducible installs across machines.

// Without lock file:
// package.json: "lodash": "^4.17.0"
// Developer A installs → gets 4.17.21
// Developer B installs (months later) → gets 4.17.25
// Different versions = potential bugs!

// With lock file:
// Pins EVERY package (including transitive) to exact version
// npm install reads lock file → same versions everywhere

// Lock file formats:
// npm    → package-lock.json
// pnpm   → pnpm-lock.yaml
// yarn   → yarn.lock
// bun    → bun.lockb (binary)

// Rules:
// ✓ ALWAYS commit your lock file to git
// ✓ NEVER edit lock files manually
// ✓ Use 'npm ci' in CI/CD (clean install from lock file)
// ✓ Use only ONE package manager per project

// npm install vs npm ci:
// npm install — updates lock file if package.json changed
// npm ci      — installs EXACTLY what's in lock file
//              (fails if lock file is out of sync)
//              Use this in CI/CD pipelines!

// pnpm equivalent:
// pnpm install --frozen-lockfile
// (fails if lock file needs updating)

// Detecting lock file drift:
// If package.json and lock file disagree:
// npm ci will FAIL (which is what you want in CI)
// This catches "forgot to run npm install" mistakes

Workspaces

Manage multiple packages in one repository (monorepo).

// Monorepo: multiple related packages in one git repo
// Example: a shared UI library + web app + API server

// Structure:
// my-project/
// ├── package.json          ← root (workspace config)
// ├── pnpm-workspace.yaml   ← pnpm workspace definition
// ├── packages/
// │   ├── ui/               ← shared component library
// │   │   ├── package.json
// │   │   └── src/
// │   ├── web/              ← Next.js app
// │   │   ├── package.json
// │   │   └── src/
// │   └── api/              ← Express server
// │       ├── package.json
// │       └── src/

// pnpm-workspace.yaml:
packages:
  - "packages/*"

// Root package.json:
{
  "private": true,
  "scripts": {
    "dev": "pnpm -r dev",
    "build": "pnpm -r build",
    "lint": "pnpm -r lint"
  }
}

// packages/web/package.json:
{
  "name": "@myproject/web",
  "dependencies": {
    "@myproject/ui": "workspace:*"
  }
}

// Benefits:
// - Shared code without publishing to npm
// - One git repo, one CI pipeline
// - Atomic changes across packages
// - Single node_modules (pnpm links efficiently)

// Commands:
// pnpm -r dev              → run "dev" in ALL packages
// pnpm --filter web dev    → run "dev" only in web package
// pnpm --filter ui build   → build only the UI package
// pnpm add lodash --filter api  → add lodash only to api

Updating

Keep dependencies fresh without breaking things.

// Check for outdated packages:
$ npm outdated
// Package  Current  Wanted  Latest
// lodash   4.17.20  4.17.21  4.17.21
// react    18.2.0   18.2.0   19.0.0

// Update within semver range (safe):
$ npm update           // updates all to "wanted" version
$ pnpm update          // same

// Update to latest (may include breaking changes):
$ npm install react@latest
$ pnpm update --latest  // updates everything to latest

// Interactive update (pnpm):
$ pnpm update --interactive
// Shows each package, lets you choose which to update

// Automated updates:
// - GitHub Dependabot: auto-creates PRs for updates
// - Renovate Bot: more configurable alternative
// Both run your tests to verify updates don't break anything

// Handling major version updates (breaking changes):
// 1. Read the changelog/migration guide
// 2. Create a branch
// 3. Update the package
// 4. Fix breaking changes (compiler/tests will guide you)
// 5. Run full test suite
// 6. Merge when green

// Pinning a specific version (prevent auto-updates):
// Remove ^ or ~ prefix:
"lodash": "4.17.21"  // exactly this version, never update

// Overrides — force a version for transitive dependencies:
// package.json:
{
  "pnpm": {
    "overrides": {
      "vulnerable-package": ">=2.0.1"
    }
  }
}

Best Practices

Professional dependency management habits.

// 1. Use exact lock file installs in CI:
// npm ci (not npm install)
// pnpm install --frozen-lockfile

// 2. Audit regularly:
$ pnpm audit
// Fix vulnerabilities before deploying

// 3. Minimize dependencies:
// Before installing: "Can I write this in 10 lines?"
// Use bundlephobia.com to check package size
// Prefer smaller, focused packages over huge utilities

// 4. Keep dependencies up to date:
// Monthly update cycle → easier than yearly catch-up
// Use Dependabot or Renovate for automation

// 5. Use .npmrc for project config:
// .npmrc file in project root:
engine-strict=true
auto-install-peers=true
save-exact=true

// 6. Set engine requirements:
// package.json:
{
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=9.0.0"
  }
}

// 7. Use corepack (Node 20+):
// $ corepack enable
// Ensures correct package manager version
// package.json:
{
  "packageManager": "pnpm@9.1.0"
}
// Anyone who clones gets the exact pnpm version

// 8. Review lock file diffs in PRs:
// Large lock file changes = red flag
// New dependencies should be intentional
// Check for supply chain attack indicators

// 9. Separate prod from dev dependencies:
// pnpm add express        → dependencies (ships to production)
// pnpm add -D vitest      → devDependencies (dev only)

FAQ

Common questions about package management.