Skip to content

13.5 Reducing Dependency Surface Area

When Azer Koçulu unpublished the 11-line left-pad package in March 2016, thousands of builds broke worldwide. The incident revealed an uncomfortable truth: the JavaScript ecosystem had become dependent on trivially small packages that developers could have written themselves in minutes. Beyond the operational disruption, each unnecessary dependency represents attack surface—code you didn't write, can't fully audit, and must perpetually maintain.

This section advocates for dependency minimalism: the practice of deliberately reducing the number of dependencies in your projects to decrease security risk, improve maintainability, and increase operational resilience.

The Minimalism Principle

Every dependency carries costs beyond its functionality:

  • Security exposure: Each package is a potential attack vector
  • Maintenance burden: Updates, vulnerability patches, breaking changes
  • Transitive risk: Dependencies bring their own dependencies
  • Availability dependency: Your builds depend on external infrastructure
  • Audit complexity: More code to review and understand

Quantifying the Risk:

Consider a project with 100 direct dependencies, each averaging 10 transitive dependencies:

Direct dependencies:           100
Transitive dependencies:      ~900
Total attack surface:       ~1,000 packages

If 1% of packages have a vulnerability per year:
Expected vulnerabilities:       ~10/year

With 50 dependencies instead:
Total packages:               ~500
Expected vulnerabilities:      ~5/year

Halving your dependencies roughly halves your vulnerability exposure.

Research Findings:

Studies of npm ecosystem security consistently find:

  • Vulnerability likelihood increases with dependency count
  • Transitive dependencies are the primary source of vulnerabilities
  • Most projects use a small fraction of their dependencies' functionality

Dependency audits frequently reveal that applications use only a small fraction (often under 5%) of a library's functionality while incurring the security cost of the entire codebase. Targeted imports and tree-shaking can significantly reduce the dependency footprint.

Identifying Unnecessary Dependencies

Before reducing dependencies, you need to find which ones are truly unnecessary.

Categories of Unnecessary Dependencies:

Category Example Why It's a Problem
Unused Installed but never imported Pure waste
Dev-only in prod Test frameworks in production bundle Unnecessary attack surface
Duplicated Two libraries for same purpose Redundant risk
Overkill Full framework for one function Excessive code for minimal use
Trivial One-liner packages Should be inline code

Detection Tools:

npm/JavaScript:

# depcheck: Find unused dependencies
npx depcheck

# npm-check: Interactive update and prune
npx npm-check

# Webpack Bundle Analyzer: Visualize what's in your bundle
npx webpack-bundle-analyzer stats.json

Python:

# pip-autoremove: Find and remove unused packages
pip install pip-autoremove
pip-autoremove --list

# pipreqs: Generate requirements from imports
pipreqs /path/to/project --force
# Compare with existing requirements.txt

General:

# Static analysis: Search for unused imports
grep -r "import.*from 'package'" src/ | wc -l
# vs.
grep "package" package.json

Detection Workflow:

  1. Run unused dependency detector
  2. Verify each flagged dependency isn't dynamically loaded
  3. Remove unused dependencies one at a time
  4. Run tests after each removal
  5. Commit incrementally for easy rollback

The Micro-Dependency Anti-Pattern

The npm ecosystem developed a culture of micro-dependencies: packages containing trivially small amounts of code.

Infamous Examples:

Package Lines of Code Weekly Downloads
is-odd 4 400,000+
is-even 3 (calls is-odd) 100,000+
left-pad 11 (removed in 2016)
is-number 18 60,000,000+
is-positive-integer ~10 1,000,000+

The is-even Absurdity:

// The entire is-even package:
module.exports = function isEven(n) {
  return !isOdd(n);  // Depends on is-odd
};

// What you could write:
const isEven = n => n % 2 === 0;

Installing is-even brings is-odd as a transitive dependency—two packages and two maintainers to trust for a one-line function.

Why Micro-Dependencies Are Risky:

  • Maintainer trust surface: Each package is a trust decision
  • Supply chain attacks: More targets for attackers (see event-stream)
  • Availability: More points of failure
  • Disproportionate risk: Trivial functionality, non-trivial risk

When to Inline Instead:

If a package: - Contains less than 50 lines of code - Has no complex logic or algorithms - Doesn't require specialized expertise - Can be implemented in 15 minutes or less

...you should probably write it yourself.

Replacing Heavy Dependencies

Sometimes you need the functionality but not the weight. Look for lighter alternatives.

Replacement Strategies:

1. Modular Imports:

Instead of importing entire libraries:

// Heavy: imports all of lodash
import _ from 'lodash';
_.debounce(fn, 300);

// Light: imports only what you need
import debounce from 'lodash/debounce';
debounce(fn, 300);

// Lightest: use a focused alternative
import debounce from 'debounce';  // Smaller single-purpose package

2. Native Alternatives:

Modern JavaScript/platforms often provide what packages once did:

Package Native Alternative
moment Intl.DateTimeFormat, Date methods
underscore Array/Object methods, spread operator
node-uuid crypto.randomUUID()
request fetch (native)
left-pad String.prototype.padStart()

3. Lighter Libraries:

Heavy Lighter Alternative
moment (289KB) date-fns (modular) or dayjs (2KB)
lodash (70KB) lodash-es (tree-shakeable) or individual functions
axios (29KB) ky (8KB) or native fetch
express fastify or koa (depending on needs)

Evaluation Criteria:

Before replacing:

  1. Does the alternative have adequate security practices?
  2. Is it actively maintained?
  3. Does it cover your actual use cases?
  4. What's the migration effort?

A smaller but abandoned package may be worse than a larger maintained one.

Build vs. Borrow Criteria

Deciding when to implement functionality yourself versus using a dependency requires balancing multiple factors.

Build In-House When:

Factor Build Indicator
Complexity Simple, well-understood logic
Size Less than 100 lines of code
Expertise Within team's competence
Stability Requirements unlikely to change
Criticality Security-sensitive, needs control
Alternatives Available packages are heavy/poorly maintained

Borrow (Use Dependency) When:

Factor Borrow Indicator
Complexity Complex algorithms, edge cases
Size Substantial implementation effort
Expertise Requires specialized knowledge (crypto, protocols)
Stability Evolving standards requiring updates
Criticality Battle-tested code preferred
Alternatives Well-maintained, focused packages exist

Decision Framework:

Is this security-sensitive (crypto, auth, etc.)?
├── Yes → Use battle-tested library
└── No → Could I implement this in 1 hour?
          ├── Yes → How many packages does it bring?
          │         ├── > 5 transitive → Implement yourself
          │         └── < 5 → Consider implementing anyway
          └── No → Evaluate packages using Section 13.1 criteria

A useful heuristic: if you can explain the implementation to a junior developer in five minutes, you probably don't need a package for it.

Auditing Transitive Dependencies

Direct dependencies are only part of the story. Transitive dependencies often outnumber them 10:1.

Audit Approach:

# npm: Full dependency tree
npm ls --all

# Depth analysis
npm ls --all | grep -E "^[│├└]" | head -100

# Why is this package here?
npm explain lodash

# Python: Show what requires what
pipdeptree --reverse --packages requests

Reduction Techniques:

1. Find Duplicate Functionality:

# Look for multiple packages doing the same thing
npm ls | grep -E "(request|axios|got|node-fetch)"

If you have three HTTP libraries, consolidate to one.

2. Identify Heavy Transitive Dependencies:

# npm: Package sizes
npx package-size lodash moment axios

# Analyze bundle
npx source-map-explorer bundle.js

If a direct dependency brings heavy transitive packages, consider alternatives.

3. Version Deduplication:

# npm: Deduplicate
npm dedupe

# Check for multiple versions
npm ls lodash  # Shows all versions in tree

Multiple versions of the same package increase attack surface.

The Cultural Challenge

Dependency minimalism often conflicts with developer expectations of convenience.

Developer Perspective:

  • "Why reinvent the wheel?"
  • "The package already exists and works"
  • "I don't have time to implement this"
  • "Using packages is best practice"

Security Perspective:

  • "Every package is a trust decision"
  • "We're responsible for code we ship"
  • "Convenience isn't worth unlimited risk"
  • "Less code means less attack surface"

Making the Case:

Frame minimalism as productivity, not restriction:

  1. Fewer vulnerabilities to patch: Less interrupt-driven work
  2. Smaller bundle sizes: Faster builds and deploys
  3. Simpler debugging: Less code to understand
  4. Reduced maintenance: Fewer breaking changes to handle
  5. Better understanding: You know what your code does

Dependency Budgets:

Some organizations implement dependency budgets—limits on dependency counts or sizes:

# Example dependency budget policy
dependency-policy:
  max_direct_dependencies: 50
  max_total_dependencies: 200
  max_bundle_size_kb: 500

  exceptions:
    require_justification: true
    require_approval: ["@security-team"]

Budgets make the cost of dependencies visible and require conscious decisions to exceed them.

Governance Approach:

  1. Set reasonable initial budgets based on current state
  2. Require justification for budget increases
  3. Review budget usage in sprint retrospectives
  4. Celebrate successful dependency reduction

Recommendations

For Developers:

  1. Question every npm install. Before adding a dependency, ask: Is this necessary? Can I implement it? Is there a lighter alternative?

  2. Audit your imports. Regularly run unused dependency detection. Remove what you don't use.

  3. Replace trivial packages. If it's under 20 lines, write it yourself. You'll understand it better and trust it more.

  4. Prefer modular imports. Import specific functions, not entire libraries. Enable tree-shaking.

For Engineering Managers:

  1. Make dependency costs visible. Track dependency counts as a code health metric. Include in dashboards.

  2. Celebrate reduction. Recognize when developers reduce dependencies. Make minimalism a positive value.

  3. Set reasonable budgets. Establish dependency limits that encourage thoughtfulness without creating bureaucracy.

  4. Allocate time for cleanup. Schedule periodic dependency audits. Treat reduction as legitimate work.

For Organizations:

  1. Define dependency philosophy. Establish organizational stance on dependency minimalism. Document expectations.

  2. Provide alternatives. If you discourage certain packages, suggest or provide alternatives.

  3. Include in code review. Add dependency additions to code review checklist. Question new packages.

  4. Measure and track. Monitor dependency metrics over time. Set reduction goals.

Dependency minimalism isn't about rejecting all external code—it's about being intentional. Every dependency should earn its place in your project, providing value that justifies its cost in security exposure, maintenance burden, and operational risk. The goal isn't zero dependencies; it's zero unnecessary dependencies.