0x55aa
โ† Back to Blog

NPM Dependency Hell: A Survival Guide ๐Ÿ“ฆ

โ€ข12 min read

NPM Dependency Hell: A Survival Guide ๐Ÿ“ฆ

Confession time: I once added ONE package to a Node.js project at Acodez. ONE! I ran npm install express-rate-limit and watched in horror as npm downloaded 237 additional packages. My node_modules folder went from 150MB to 380MB. The build took 4 minutes instead of 30 seconds. And the best part? Three different versions of lodash. THREE! ๐Ÿคฏ

Coming from Laravel where Composer dependencies are... let's say "more reasonable," the npm ecosystem felt like the Wild West. Everyone's installing everything, packages depend on packages that depend on packages, and before you know it, your node_modules folder is bigger than your actual codebase!

Let me share the hard lessons I learned about npm dependency management so you don't make the same mistakes!

The Node_Modules Horror Story ๐Ÿ’€

First, let's talk about the elephant in the room:

# A fresh Express project
npm init -y
npm install express

# What you expected:
# node_modules/
# โ””โ”€โ”€ express/

# What you actually got:
du -sh node_modules/
# 42MB  node_modules/

ls node_modules/ | wc -l
# 57 packages

# You installed ONE package and got 56 dependencies! ๐ŸŽ‰

The infamous node_modules joke:

# Heaviest objects in the universe:
# 1. Neutron stars
# 2. Black holes
# 3. node_modules folder

# True story: My node_modules was so big, it created its own gravity well!

Coming from Laravel: Composer vendor folders are big, sure. But npm takes it to another level. It's not uncommon to have 1000+ packages in node_modules for a medium-sized project!

Package.json vs Package-lock.json ๐Ÿ”’

Here's what confused me for MONTHS:

// package.json
{
  "dependencies": {
    "express": "^4.18.0"  // The ^ is IMPORTANT!
  }
}

// That little ^ means:
// "Give me express 4.18.0 OR ANY COMPATIBLE VERSION"
// Compatible = 4.18.1, 4.19.0, 4.99.0
// NOT compatible = 5.0.0

What happens when you npm install:

# Developer 1 (January 2026):
npm install
# Gets express 4.18.0

# Developer 2 (March 2026):
npm install
# Gets express 4.21.0  # New patch version!

# Same package.json, DIFFERENT CODE! ๐Ÿ˜ฑ

Enter package-lock.json:

// package-lock.json
{
  "name": "your-app",
  "lockfileVersion": 2,
  "dependencies": {
    "express": {
      "version": "4.18.0",  // EXACT version locked!
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.0.tgz",
      "integrity": "sha512-..."
    }
  }
}

The golden rule I learned at Acodez:

  1. package.json = What you WANT
  2. package-lock.json = What you ACTUALLY HAVE
  3. ALWAYS commit package-lock.json to Git!
  4. Use npm ci in CI/CD (not npm install)

Why npm ci?

# npm install:
# - Uses package.json ranges
# - Might update packages
# - Can modify package-lock.json
# - Takes longer

# npm ci (Clean Install):
# - Uses package-lock.json EXACTLY
# - Deletes node_modules first
# - Reproducible builds
# - Faster (for CI)

# In your CI pipeline:
npm ci  # Not npm install!
npm test
npm run build

A mistake that haunted me: Used npm install in production CI. Builds were inconsistent. Tests passed locally, failed in CI. Spent 3 hours debugging, only to find a patch version introduced a breaking change. Switched to npm ci, never had the issue again! ๐ŸŽ‰

Semantic Versioning (The Rules Everyone Breaks) ๐Ÿ“Š

SemVer format: MAJOR.MINOR.PATCH

# Example: 4.18.2
# 4 = MAJOR version (breaking changes)
# 18 = MINOR version (new features, backwards compatible)
# 2 = PATCH version (bug fixes)

Version range symbols:

{
  "dependencies": {
    "express": "4.18.0",     // Exact version only
    "lodash": "^4.17.21",    // ^4.17.21 means >=4.17.21 <5.0.0
    "moment": "~2.29.4",     // ~2.29.4 means >=2.29.4 <2.30.0
    "axios": "*",            // ANY VERSION (NEVER DO THIS!)
    "cors": ">=2.8.5",       // Greater or equal (risky)
    "dotenv": "latest"       // ALSO NEVER DO THIS!
  }
}

What I use in production:

{
  "dependencies": {
    // Safe: Patch updates only (bug fixes)
    "express": "~4.18.0",

    // Riskier: Minor updates too (new features)
    "lodash": "^4.17.21",

    // Never in production:
    "package": "*",      // โŒ Too risky!
    "package": "latest"  // โŒ Breaks reproducibility!
  }
}

The reality check: Developers say they follow SemVer. They lie. I've seen "patch" releases break everything!

The Dreaded Dependency Conflicts ๐Ÿ”ฅ

A scenario that WILL happen to you:

npm install some-package

# npm ERR! ERESOLVE could not resolve
# npm ERR!
# npm ERR! While resolving: [email protected]
# npm ERR! Found: [email protected]
# npm ERR!
# npm ERR! Could not resolve dependency:
# npm ERR! peer react@"^17.0.0" from [email protected]

# Translation: "This package wants React 17, you have React 18. Good luck!"

Option 1: Force it (the dangerous way)

npm install --legacy-peer-deps
# OR
npm install --force

# This WILL install the package.
# It also MIGHT break everything.
# Your tests will let you know! ๐Ÿคž

Option 2: Find a compatible version

# Check which versions are compatible
npm view some-package versions

# Try older version
npm install [email protected]

# Or update React (might break other things)
npm install [email protected]

Option 3: Use npm overrides (npm 8.3+)

// package.json
{
  "overrides": {
    "some-package": {
      "react": "^18.0.0"
    }
  }
}

// Forces some-package to use React 18
// Use with caution!

A pattern I learned the hard way:

# Before adding ANY package:
npm info package-name peerDependencies

# Check if it conflicts with your dependencies!
# Saves hours of debugging later!

NPM Audit: Security Theater or Real Protection? ๐Ÿ”’

You've seen this:

npm install

# added 247 packages, and audited 248 packages in 12s
#
# 89 vulnerabilities (12 low, 34 moderate, 31 high, 12 critical)
#
# To address issues that do not require attention, run:
#   npm audit fix
#
# To address all issues (including breaking changes), run:
#   npm audit fix --force

# You: ๐Ÿ˜ฐ

My process for handling npm audit:

# Step 1: Don't panic
npm audit

# Step 2: Check WHAT'S vulnerable
npm audit --json > audit.json

# Step 3: Check if YOU'RE affected
# (Most vulnerabilities are in dev dependencies or unused code paths)

# Step 4: Safe fixes first
npm audit fix

# Step 5: Manual review for breaking changes
npm audit fix --dry-run

# Step 6: NEVER blindly run --force!
# npm audit fix --force  # โŒ This WILL break things!

Real talk about vulnerabilities:

// Example npm audit warning:
// "lodash <4.17.21 has a prototype pollution vulnerability"

// Questions to ask:
// 1. Is lodash in dependencies or devDependencies?
// 2. Do I even USE lodash, or is it a sub-dependency?
// 3. Does the vulnerability affect my code paths?
// 4. Can I update without breaking changes?

// A vulnerability in a dev dependency (like a testing tool)?
// Probably not worth breaking your build over!

// A vulnerability in a production dependency you actually use?
// FIX IT NOW!

A mistake at Acodez: Ran npm audit fix --force before a production deploy. Updated everything. EVERYTHING broke. Tests failed. Build failed. Reverted frantically. Learned to check audit reports carefully! ๐Ÿ™ˆ

Deduplication: Taming the Chaos ๐Ÿงน

Here's a fun npm quirk:

# You have:
node_modules/
โ”œโ”€โ”€ package-a/
โ”‚   โ””โ”€โ”€ node_modules/
โ”‚       โ””โ”€โ”€ [email protected]/
โ””โ”€โ”€ package-b/
    โ””โ”€โ”€ node_modules/
        โ””โ”€โ”€ [email protected]/

# Three copies of lodash!
# In the SAME PROJECT!

Solution: npm dedupe

npm dedupe

# Flattens dependency tree
# Removes duplicate packages
# Can reduce node_modules size by 20-30%!

# After:
node_modules/
โ”œโ”€โ”€ [email protected]/  # Shared by everyone
โ”œโ”€โ”€ package-a/
โ””โ”€โ”€ package-b/

Add to your workflow:

# After updating dependencies:
npm update
npm dedupe
npm audit fix

# Then commit the updated package-lock.json!

Phantom Dependencies (The Hidden Trap) ๐Ÿ‘ป

A bug that confused me for DAYS:

// Your code
const _ = require('lodash');

// You: "But I didn't install lodash?!"
// npm: "One of your dependencies did! You're using it anyway!"

// This works in dev...
// Then you remove the parent package...
// And everything breaks in production!

The problem:

// package.json (what YOU installed)
{
  "dependencies": {
    "express": "^4.18.0"
    // No lodash here!
  }
}

// But express depends on lodash...
// So lodash is in node_modules...
// So you CAN import it...
// Even though you SHOULDN'T!

The fix:

# Explicitly install what you use!
npm install lodash

# Now it's in YOUR package.json
# And won't disappear when express changes!

A pattern I follow:

// If I import it, I install it!
// No phantom dependencies allowed!

// Check for phantom dependencies:
npx depcheck

# Shows packages you:
# - Import but didn't install (phantom deps)
# - Installed but don't use (bloat)

The Monorepo Package Management Maze ๐Ÿข

When I was building microservices at Acodez with a monorepo:

# Project structure:
monorepo/
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ””โ”€โ”€ package.json
โ”‚   โ”œโ”€โ”€ web/
โ”‚   โ”‚   โ””โ”€โ”€ package.json
โ”‚   โ””โ”€โ”€ shared/
โ”‚       โ””โ”€โ”€ package.json
โ””โ”€โ”€ package.json

# Problem: Each package has node_modules?!
# 3 projects ร— 40MB = 120MB of duplicates!

Solution: Workspaces

// Root package.json
{
  "name": "monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

// npm automatically:
// - Links shared packages
// - Deduplicates dependencies
// - One node_modules at root

Usage:

# Install ALL workspace dependencies
npm install

# Run command in specific workspace
npm run build --workspace=packages/api

# Add dependency to specific workspace
npm install express --workspace=packages/api

Coming from Laravel: Composer doesn't have great monorepo support. npm workspaces (or pnpm/yarn workspaces) are actually really good for this! ๐ŸŽ‰

Best Practices I Actually Follow ๐Ÿ“

1. Lock Your Dependencies

// โœ… GOOD: In production apps
{
  "dependencies": {
    "express": "4.18.2"  // Exact version
  }
}

// โŒ BAD: In production apps
{
  "dependencies": {
    "express": "^4.18.2"  // Risky
  }
}

// Note: Libraries should use ranges, apps should use exact versions!

2. Separate Dev and Prod Dependencies

{
  "dependencies": {
    // Production code NEEDS these
    "express": "4.18.2",
    "mongoose": "7.0.0"
  },
  "devDependencies": {
    // Only for development/testing
    "nodemon": "2.0.20",
    "jest": "29.0.0",
    "@types/express": "4.17.17"
  }
}

// In production:
npm ci --omit=dev  # Doesn't install devDependencies!

3. Review Dependencies Before Installing

# Before: npm install some-random-package

# After (what I do now):
npm info some-random-package

# Check:
# - Weekly downloads (is it popular?)
# - Last publish date (is it maintained?)
# - Dependencies (how many sub-dependencies?)
# - License (is it compatible?)

# Too many red flags? Find an alternative!

4. Use .npmrc for Team Consistency

# .npmrc in project root
save-exact=true                # No ^ or ~ in package.json
engine-strict=true             # Enforce Node version
legacy-peer-deps=false         # Don't allow peer dep issues
audit-level=moderate           # Warn on moderate+ vulnerabilities

# Commit this file to Git!
# Now everyone uses same npm settings!

5. Update Regularly (But Carefully)

# Check outdated packages
npm outdated

# Update patch versions only (safe)
npm update

# Update to latest (breaking changes possible)
npx npm-check-updates -u
npm install

# ALWAYS test after updating!
npm test

Tools That Save My Life ๐Ÿ› ๏ธ

1. npm-check-updates

npm install -g npm-check-updates

# See what's outdated
ncu

# Update package.json to latest versions
ncu -u

# Then install
npm install

2. depcheck (find unused dependencies)

npx depcheck

# Shows:
# - Unused dependencies (remove them!)
# - Phantom dependencies (install them!)

3. bundlephobia (check package sizes)

# Before installing, check size:
# https://bundlephobia.com/package/some-package

# Is 300KB too much for a date formatter?
# Maybe find a lighter alternative!

4. npm-why (understand dependency tree)

npm install -g npm-why

npm-why lodash

# Shows: "You have lodash because express needs it"
# Useful for debugging conflicts!

Quick Wins (Do These Today!) ๐Ÿƒโ€โ™‚๏ธ

  1. Run npm dedupe - Reduce node_modules size by 20-30%
  2. Check for phantom dependencies - npx depcheck
  3. Add .npmrc - Enforce team standards
  4. Use npm ci in CI/CD - Reproducible builds
  5. Review npm audit - Fix real security issues

Your NPM Checklist โœ…

Before committing:

  • package-lock.json committed to Git
  • All imports have matching dependencies in package.json
  • No unused dependencies (check with depcheck)
  • npm audit reviewed (critical issues fixed)
  • npm dedupe run (no duplicate packages)
  • .npmrc configured for team consistency
  • Tests pass after dependency changes
  • Production uses npm ci --omit=dev

The Bottom Line

NPM is chaos. But it's MANAGEABLE chaos!

The essentials:

  1. Commit package-lock.json - Reproducible builds save lives
  2. Use npm ci in CI/CD - Not npm install
  3. Lock production dependencies - No surprises in prod
  4. Review before installing - Not all packages are worth it
  5. Update regularly - But test thoroughly

When I was building Node.js APIs at Acodez, I learned that npm dependency management is like gardening. You can't just plant seeds (install packages) and walk away. You need to weed (remove unused deps), prune (update carefully), and protect against pests (security vulnerabilities). Neglect it, and your garden (project) becomes overgrown! ๐ŸŒฑ

Coming from Laravel's Composer, npm felt overwhelming at first. More packages, more versions, more conflicts. But the ecosystem is also MUCH bigger - there's a package for everything! The key is being selective and keeping dependencies under control! ๐ŸŽฏ

Think of npm like a magic package vending machine. You can get ANYTHING you want! But if you're not careful, you'll end up with 1,247 packages, three versions of lodash, and a node_modules folder that achieves sentience! ๐Ÿค–


Got dependency horror stories? Share them on LinkedIn - everyone's been there!

Want to see my Node.js projects? Check out my GitHub - with reasonably-sized node_modules folders! ๐Ÿ˜‰

P.S. - If your node_modules folder is bigger than your source code, you might want to run depcheck and npm dedupe RIGHT NOW. Your disk space (and build server) will thank you! ๐Ÿ“ฆโœจ