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 librariesLock 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 apiUpdating
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.