Skip to content

mod_06 - Managing Dependencies

Dependencies are not free - every one you add is code running in your process with full access to your application's memory and filesystem
Node's dependency model makes it trivially easy to add packages (one npm install away) and trivially easy to accumulate hundreds of transitive dependencies that you've never reviewed. Managing dependencies means understanding what you're pulling in, keeping them updated without breaking things , and knowing when to lock them down or push them out

npm update and outdated

# Check for outdated packages
npm outdated

# Example output:
# Package  Current  Wanted  Latest  Location
# express  4.17.1  4.18.2  4.18.2  my-app
# lodash   4.17.20 4.17.21 4.17.21 my-app
# jest     28.0.0  28.5.1  29.5.0  my-app

# Update to the "Wanted" version (respects semver ranges in package.json)
npm update

# Update a specific package
npm update express

# Install a specific version (overrides semver range)
npm install express@4.18.2

npm outdated shows three version columns: * Current - what's installed in node_modules * Wanted - the latest version matching your package.json semver range * Latest - the newest published version (may be outside your range)

If you use ^4.17.1, the "Wanted" column shows 4.18.2 - the latest 4.x version. "Latest" might show 5.0.0 - which requires a major upgrade

npm update vs manual upgrade

npm update only updates within your existing semver range. If you want to jump major versions:

# Upgrade to a specific major version
npm install express@5

npm dedupe

Removes duplicate packages by hoisting them

npm dedupe

# npm will try to flatten the dependency tree:
# Before: a -> lodash@4.17.20 (in a's node_modules)
#         b -> lodash@4.17.20 (in b's node_modules)
# After:  lodash@4.17.20 (hoisted to top node_modules)
#         a and b both resolve to the same instance
# 
# ```mermaid
# graph LR
#     subgraph "Before dedupe"
#         A["a"] --> AL["lodash@4.17.20<br/>in a's node_modules"]
#         B["b"] --> BL["lodash@4.17.20<br/>in b's node_modules"]
#     end
#     subgraph "After dedupe"
#         C["a"] --> TOP["lodash@4.17.20<br/>hoisted to top node_modules"]
#         D["b"] --> TOP
#     end
# ```

npm v7+ does this automatically during install. But running npm dedupe after manual interventions helps clean up

peer dependencies

Peer dependencies let a package specify that it needs a certain version of another package , but doesn't install it itself - the consumer must provide it

{
  "name": "express-middleware-auth",
  "version": "2.0.0",
  "peerDependencies": {
    "express": "^4.0.0"
  }
}

This says: "I work with Express 4.x and you need to have express installed in your project." The package does NOT install express itself - it expects the consuming project to have it

Peer dependencies are used for: * Plugins - a Passport strategy needs Passport installed * Frameworks - a React component needs React installed * Tooling - an ESLint plugin needs ESLint installed

{
  "name": "eslint-plugin-security",
  "peerDependencies": {
    "eslint": ">=8.0.0"
  }
}

npm v7+ installs peer dependencies automatically if they're not already present. npm v6 only warns. In v7+, if the peer dependency version range conflicts with an existing dependency, npm fails the install - which is stricter but more deterministic

bundled and optional dependencies

bundledDependencies

Dependencies bundled with your package when publishing

{
  "name": "my-cli-tool",
  "dependencies": {
    "native-addon": "^1.0.0"
  },
  "bundledDependencies": ["native-addon"]
}

Bundled dependencies are included in your published tarball - useful for: * Native addons that are painful to rebuild at install time * Packages from private registries * Vendored dependencies that need to ship together

The consumer doesn't fetch bundled deps from the registry - they come with your package. This means faster installs but larger package size

optionalDependencies

Dependencies that can fail to install without breaking the install

{
  "optionalDependencies": {
    "fsevents": "^2.3.0"
  }
}

If fsevents fails to install (e.g., on Linux), npm install continues without error. Your code must handle the case where the optional dependency is missing:

let fsevents
try {
  fsevents = require('fsevents')
} catch (err) {
  // optional dependency not available - use fallback
}

Common use: platform-specific packages like fsevents (macOS file watcher , Linux doesn't need it)

lockfile management

package-lock.json is the source of truth for your installed tree

When lockfile conflicts happen

You add a dependency , Ahmed adds another , merge conflicts happen in package-lock.json

How to resolve:

# 1. Accept the conflicting lockfile
git checkout --theirs package-lock.json

# 2. Regenerate a consistent lockfile
npm install

# The install command rewrites package-lock.json with a combined dependency tree
# that includes both your changes and Ahmed's changes

Don't manually edit package-lock.json. The JSON structure is fragile and nested - one wrong byte and the file is invalid. Always regenerate via npm install

When to regenerate lockfile completely

# Delete and regenerate
rm package-lock.json
npm install

# This only when you're confident the lockfile is corrupted
# You lose the pinned versions - everyone gets fresh resolution

Don't delete package-lock.json casually. It pins every transitive dependency. Removing it means every install resolves to potentially different versions. This is how production-only bugs get introduced

monorepo patterns: npm workspaces

Monorepos (multiple packages in a single repository) use npm workspaces

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

Directory structure:

flowchart TD
    root["my-monorepo/"] --> pkg["package.json<br/>root with workspaces config"]
    root --> pkgs["packages/"]
    pkgs --> api["api/"]
    api --> apipkg["package.json<br/>name: @my/api"]
    api --> apisrc["src/"]
    pkgs --> web["web/"]
    web --> webpkg["package.json<br/>name: @my/web"]
    web --> websrc["src/"]
    pkgs --> shared["shared/"]
    shared --> spkg["package.json<br/>name: @my/shared"]
    shared --> ssrc["src/"]

Workspace commands:

# Install all dependencies for all workspaces
npm install

# Run a script in a specific workspace
npm run test -w @my/api

# Run a script in all workspaces
npm run test --workspaces

# Add a dependency to a specific workspace
npm install lodash -w @my/api

# Run scripts in parallel across workspaces
npm run build --workspaces --if-present

npm workspaces hoist dependencies to the root node_modules when versions are compatible. This reduces disk space and gives you a single lockfile for the entire project

security: transitive dependencies

Your direct dependencies have dependencies , and those have dependencies. You're running code from dozens or hundreds of authors

# Count transitive dependencies
npm ls --all | wc -l  # might shock you

# Find which package depends on a specific sub-dependency
npm explain lodash

# Audit all transitive dependencies
npm audit

# Check for duplicate packages (wasted space and potential conflicts)
npm dedupe --dry-run

Deprecation warnings

When a package is deprecated, npm prints a warning during install

npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained

Don't ignore these. A deprecated package means no security patches. Run npm outdated and find alternatives

Lockfile auditing

# Check what changed in the lockfile
git diff package-lock.json

# Check integrity of installed packages
npm cache verify

When reviewing a PR that updates dependencies , look at: * What packages changed (not just versions - new additions) * Where major versions changed (breaking changes) * What postinstall scripts were added

Tools for dependency management

# npm audit - known vulnerabilities
npm audit

# npm outdated - versions behind
npm outdated

# Make status - installed tree
npm ls

# Why is this dependency here?
npm explain some-package

For comprehensive dependency management , consider:

# Automated update tool
npx npm-check-updates

# Dependency dashboard (npm install -g npm-check)
npx npm-check

summary

  • npm outdated shows Current vs Wanted vs Latest versions
  • npm update respects semver ranges - use npm install for major upgrades
  • Peer dependencies: plugin expects consumer to provide the dependency
  • Bundled dependencies ship with your package; optional deps can fail gracefully
  • Don't manually edit package-lock.json - regenerate via npm install
  • npm workspaces manage monorepo dependencies with hoisting
  • Every transitive dependency is a supply chain risk - audit regularly
  • Deprecation warnings mean no more security patches - update or replace

prerequisites

mod_05_scripts.md - npm scripts and lifecycle hooks

next -> mod_07_publish_packages.md