NPM Dependency Resolution: A Visual Guide
What is Dependency Resolution?
When you run npm install
, npm needs to figure out where to place each package in your node_modules
directory. The goal is to create the flattest possible tree (minimal nesting) while ensuring all packages get the correct versions they need.
Key Principles
- Flatter is better - Less nesting means smaller package size and better performance
- Installation order matters - The first package installed gets priority for top-level placement
- Compatibility first - If two packages need different versions, npm will nest them to avoid conflicts
Step-by-Step Example
Let's walk through the exact scenario from the article with directory structures and PlantUML dependency diagrams:
Step 1: Install Module-A (depends on Module-B v1.0)
Command: npm install module-a
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/
└── module-b/ (v1.0)
What happened: Both Module-A and its dependency Module-B v1.0 are installed at the top level (flat structure).
Step 2: Install Module-C (depends on Module-B v2.0)
Command: npm install module-c
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/
├── module-b/ (v1.0) ← Already exists at top level
└── module-c/
└── node_modules/
└── module-b/ (v2.0) ← Nested because v1.0 is already at top
What happened: Module-C is installed at top level, but its dependency Module-B v2.0 is nested because Module-B v1.0 already occupies the top level.
Step 3: Install Module-D (depends on Module-B v2.0)
Command: npm install module-d
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/
├── module-b/ (v1.0)
├── module-c/
│ └── node_modules/
│ └── module-b/ (v2.0)
└── module-d/
└── node_modules/
└── module-b/ (v2.0) ← Another copy! Duplication occurs
What happened: Module-D also needs Module-B v2.0, but since v1.0 is at the top level, npm creates another nested copy.
Step 4: Install Module-E (depends on Module-B v1.0)
Command: npm install module-e
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/
├── module-b/ (v1.0) ← Shared by Module-A and Module-E
├── module-c/
│ └── node_modules/
│ └── module-b/ (v2.0)
├── module-d/
│ └── node_modules/
│ └── module-b/ (v2.0)
└── module-e/ ← No nested module-b needed!
What happened: Module-E can use the existing top-level Module-B v1.0, so no duplication needed.
The Update Scenario
Step 5: Update Module-A to v2.0 (now depends on Module-B v2.0)
Command: npm install [email protected]
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/ (v2.0)
│ └── node_modules/
│ └── module-b/ (v2.0) ← Nested because v1.0 still needed by Module-E
├── module-b/ (v1.0) ← Still needed by Module-E
├── module-c/
│ └── node_modules/
│ └── module-b/ (v2.0)
├── module-d/
│ └── node_modules/
│ └── module-b/ (v2.0)
└── module-e/
What happened: Module-A v2.0 needs Module-B v2.0, but v1.0 is still at the top level (needed by Module-E), so v2.0 gets nested under Module-A.
Step 6: Update Module-E to v2.0 (now depends on Module-B v2.0)
Command: npm install [email protected]
Dependency Graph:
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/ (v2.0)
│ └── node_modules/
│ └── module-b/ (v2.0)
├── module-b/ (v2.0) ← Now v2.0 at top level!
├── module-c/
│ └── node_modules/
│ └── module-b/ (v2.0) ← Still duplicated
├── module-d/
│ └── node_modules/
│ └── module-b/ (v2.0) ← Still duplicated
└── module-e/ (v2.0)
What happened:
- Module-E v1.0 removed
- Module-B v1.0 removed (no longer needed)
- Module-B v2.0 promoted to top level
- But we still have duplicates!
The Solution: npm dedupe
Command: npm dedupe
Dependency Graph (After Deduplication):
Loading Diagram...
Directory Structure:
node_modules/
├── module-a/ (v2.0)
├── module-b/ (v2.0) ← Single copy at top level
├── module-c/
├── module-d/
└── module-e/ (v2.0)
What happened: All nested copies of Module-B v2.0 are removed, and all modules now reference the single top-level copy.
Visual Summary: Complete Dependency Evolution
Here's a comprehensive PlantUML diagram showing the entire dependency resolution journey:
Loading Diagram...
Key Takeaways
- Installation order determines tree structure - First installed package gets top-level priority
- Different machines can have different trees - But your app will work the same way
- Use
npm install
from package.json for consistency - It installs in alphabetical order - Clean installs ensure consistency - Delete
node_modules
and runnpm install
to get the same tree every time - Use
npm dedupe
to optimize - Removes unnecessary duplicate packages
Best Practices
- Always use
npm install
instead of manually copying packages - When in doubt, delete
node_modules
and reinstall - Run
npm dedupe
periodically to optimize your dependency tree - Be aware that different installation orders can create different (but functionally equivalent) trees