Skip to content

Instantly share code, notes, and snippets.

@mgratzer
Created October 18, 2025 08:20
Show Gist options
  • Select an option

  • Save mgratzer/832c150c1d6e437d0335d9da5eba8e34 to your computer and use it in GitHub Desktop.

Select an option

Save mgratzer/832c150c1d6e437d0335d9da5eba8e34 to your computer and use it in GitHub Desktop.
Node.js Dependency Audit Guide - A pragmatic, minimalist approach to managing dependencies in any Node.js/TypeScript project

Node.js Dependency Audit Guide

This is a universal guide for auditing and managing dependencies in any Node.js/TypeScript project with a pragmatic, minimalist philosophy. Use this to audit any project and create a comprehensive DEPENDENCIES.md file.

Supports: npm, pnpm, yarn, bun Works with: Node.js, TypeScript, JavaScript, monorepos

Quick Start

For Claude Code

To audit a project using this guide:

Using the DEPENDENCY_AUDIT_GUIDE.md as your reference, audit this project's
dependencies and create a DEPENDENCIES.md file following the template provided.

Claude will:

  1. Run audit commands for your package manager
  2. Analyze each dependency
  3. Categorize them (🟒 Excellent / 🟑 Acceptable / πŸ”΄ Questionable)
  4. Create a project-specific DEPENDENCIES.md
  5. Identify removable dependencies
  6. Suggest version pinning strategies

Package Manager Detection

This guide works with any package manager. Commands are provided for all major tools:

  • npm - Default Node.js package manager
  • pnpm - Fast, disk-space efficient
  • yarn - Classic or Berry (v2+)
  • bun - Fast all-in-one toolkit

Most commands follow this pattern: <package-manager> <command> <args>

Philosophy

"A little copying is better than a little dependency" - Go Proverb

Apply this mindset to JavaScript: Every dependency is code you don't control, a potential security vulnerability, technical debt, and a maintenance burden. Choose wisely.

The Audit Process

Step 1: Gather Current State

Run these commands to understand the project (adjust for your package manager):

# List direct dependencies only
npm ls --depth=0              # npm
pnpm ls --depth=0             # pnpm
yarn list --depth=0           # yarn
bun pm ls                     # bun

# Check node_modules size
du -sh node_modules/

# Check for security vulnerabilities
npm audit                     # npm
pnpm audit                    # pnpm
yarn audit                    # yarn
bun pm audit                  # bun (if available)

# Find outdated packages
npm outdated                  # npm
pnpm outdated                 # pnpm
yarn outdated                 # yarn
bun outdated                  # bun (if available)

# Check TypeScript configuration (if TypeScript project)
cat tsconfig.json | grep -A 5 "compilerOptions"

Record:

  • Total direct dependencies (production + dev)
  • node_modules size
  • Security vulnerabilities (severity and count)
  • TypeScript strict mode status

Step 2: Analyze Each Dependency

For each production dependency, gather:

# Check version (any package manager)
npm view <package> version
pnpm info <package> version
yarn info <package> version
bun pm view <package> version

# Check transitive dependencies
npm view <package> dependencies
pnpm info <package> dependencies
yarn info <package> dependencies

# Check why it's installed
npm ls <package>
pnpm why <package>
yarn why <package>

# Check package details (weekly downloads, publish date, etc.)
npm view <package>
pnpm info <package>
yarn info <package>

# Or use online tools
# - https://bundlephobia.com (bundle size)
# - https://npmtrends.com (popularity trends)
# - https://www.npmjs.com/package/<package> (full details)

Step 3: Categorize Dependencies

Use this decision tree for each dependency:

🟒 EXCELLENT (Keep Without Question)

  • Zero transitive dependencies
  • Small size (<100KB)
  • Single, clear purpose
  • Would take >50 lines to reimplement
  • Examples:
    • Validation: zod, yup
    • HTTP: hono, fastify (lightweight frameworks)
    • Database: postgres.js, better-sqlite3
    • Utilities: date-fns (if you need it), ms, dotenv

🟑 ACCEPTABLE (Necessary for business logic)

  • Few transitive deps (<10)
  • Official SDK or well-maintained library
  • Core to product functionality
  • Examples:
    • ORMs: drizzle-orm, prisma (if needed)
    • Official SDKs: @google-cloud/, @aws-sdk/, stripe
    • Frontend: React, Vue, Svelte (framework dependencies)
    • Testing: vitest, jest (dev deps)
    • Build: vite, esbuild (dev deps)

πŸ”΄ QUESTIONABLE (High cost, evaluate alternatives)

  • Many transitive deps (>15)
  • Large size (>500KB)
  • Could be replaced with simpler approach
  • Brings in duplicate dependencies
  • Examples:
    • Heavy cloud SDKs (consider REST API instead)
    • All-in-one frameworks (evaluate what you actually use)
    • Deprecated packages (lodash β†’ native methods)
    • Utility libraries for trivial tasks (left-pad, is-number)

Step 4: Apply the Dependency Test

Before keeping any dependency, ask:

  1. Can I write this in <50 lines of code?

    • If YES β†’ Write it yourself
    • If NO β†’ Continue to question 2
  2. Does it have minimal dependencies itself?

    • Run: npm view <package> dependencies (or pnpm/yarn equivalent)
    • RED FLAG: >10 transitive dependencies
    • GREEN FLAG: 0-3 transitive dependencies
  3. Is it widely used and actively maintained?

    • Check: npm downloads/week (prefer >100k)
    • Check: Last update (prefer <6 months ago)
    • Check: GitHub stars and open issues
  4. What's the bundle size impact?

  5. Could I use a REST API instead?

    • Many SDKs are just wrappers around HTTP APIs
    • Sometimes fetch + 100 lines of code beats a 50-dependency SDK

Step 5: Identify Removable Dependencies

Look for:

Duplicate functionality:

# Check for multiple env loaders, loggers, http clients, etc.
npm ls | grep -i "dotenv"
npm ls | grep -i "logger"
npm ls | grep -i "http"
# (or use pnpm/yarn ls)

Unused dependencies:

# Search for imports across codebase
grep -r "from ['\"]<package-name>['\"]" . --include="*.ts" --include="*.tsx" --include="*.js"

# Or use specialized tools
npx depcheck                  # Find unused dependencies
npx npm-check                 # Interactive dependency checker

Redundant type packages:

# Check if types are already provided by peer dependencies
npm ls @types/<package>
# (or use pnpm why / yarn why)

Leftover dev dependencies:

  • Check package.json for tools you're not using
  • Look for packages from old configs or experiments

Step 6: Version Management Strategy

Categorize dependencies into pinning strategies:

Pin to Exact Versions (No Caret ^)

When:

  • Pre-1.0 packages (0.x.x versions) - minor bumps can break
  • Database layers (ORMs, query builders) - critical stability
  • Core business logic packages - breaking changes are catastrophic
  • Packages with history of breaking changes

Format: "package": "1.2.3" (no ^)

Use Caret Ranges (Allow Minor Updates)

When:

  • Mature, stable packages (v3+, good semver history)
  • Official SDKs from trusted vendors
  • Well-maintained frameworks with good semver discipline
  • Dev dependencies (always want latest tooling)

Format: "package": "^1.2.3" (allows 1.x.x updates)

Upgrade process for pinned packages:

  1. Check changelog for breaking changes
  2. Test in development environment
  3. Run full test suite
  4. Update manually: pnpm add <package>@<version>
  5. Document the upgrade in DEPENDENCIES.md

Step 7: Security Assessment

# Run security audit
npm audit                         # npm
pnpm audit                        # pnpm
yarn audit                        # yarn

# Check specific severity levels (npm/pnpm)
npm audit --audit-level=moderate
npm audit --audit-level=high
npm audit --audit-level=critical

# Fix automatically (use with caution)
npm audit fix                     # npm
pnpm audit --fix                  # pnpm (if available)
yarn audit --fix                  # yarn (deprecated, use upgrade-interactive)

For each vulnerability:

  • Identify if it's in production or dev dependencies
  • Check if it affects your usage (many vulns are in unused code paths)
  • Evaluate severity vs. effort to fix
  • Document accepted risks (with justification)

Acceptable vulnerabilities:

  • Low/Moderate in dev-only dependencies
  • Vulnerabilities in unused code paths (document why it's unused)
  • Awaiting upstream fixes (set reminder to check monthly)

Unacceptable vulnerabilities:

  • High/Critical in production dependencies
  • Known exploits in your attack surface
  • No upstream fix available (consider alternatives)

Creating the DEPENDENCIES.md File

Use this template structure:

# Dependency Management Guidelines

## Current State (Baseline)
- **Direct dependencies:** X (Y production, Z dev)
- **node_modules size:** XMB
- **Last audit:** YYYY-MM-DD
- **Security vulnerabilities:** X (severity breakdown)
- **Last cleanup:** YYYY-MM-DD

## Philosophy
> "A little copying is better than a little dependency" - Go Proverb

[Your project-specific philosophy paragraph]

## Before Adding a Dependency

Ask these questions in order:

1. **Can I write this in <50 lines of code?**
   - If yes: Write it yourself
   - Examples: String utilities, simple validators, array helpers

2. **Does it have minimal dependencies itself?**
   - Run: `npm view <package> dependencies` (or pnpm/yarn equivalent)
   - Red flag: >10 dependencies
   - Check: When was it last updated?

3. **Is it widely used and trusted?**
   - Check npm downloads/week
   - Check GitHub stars and issues
   - Prefer: >100k weekly downloads OR official SDK

4. **What's the bundle size impact?**
   - Check: https://bundlephobia.com
   - Red flag: >10MB for a utility library

## Version Management

### Pinned Dependencies (Exact Versions)
**These are pinned to prevent breaking changes:**

```json
// Pre-1.0 packages (0.x.x can break on minor updates)
"package-name": "0.20.2",

// Database layer (critical stability)
"orm-package": "1.44.6",

// Core business logic
"core-package": "5.0.76"
```

**To upgrade pinned dependencies:**
1. Check changelog for breaking changes
2. Test in development thoroughly
3. Update manually:
   - `npm install <package>@<version>` (npm)
   - `pnpm add <package>@<version>` (pnpm)
   - `yarn add <package>@<version>` (yarn)
   - `bun add <package>@<version>` (bun)
4. Update this document with new version

### Flexible Dependencies (Caret Ranges)
**These can auto-update within their major version:**

```json
// Mature, stable frameworks
"framework": "^4.10.1",

// Official SDKs (good semver discipline)
"official-sdk": "^7.17.2"
```

### Dev Dependencies
**All dev dependencies use caret ranges** - always get latest tooling improvements.

### When to upgrade
- **Pinned packages:** Manual upgrade only, after testing
- **Flexible packages (patch):** Auto-apply via update command
- **Flexible packages (minor):** Review changelog, test locally
- **Major versions:** Read migration guide, block time for testing

**Update commands:**
```bash
npm update                        # npm
pnpm update                       # pnpm
yarn upgrade                      # yarn
bun update                        # bun
```

## Security Protocol

### Monthly (Set calendar reminder)
```bash
npm audit && npm outdated         # npm
pnpm audit && pnpm outdated       # pnpm
yarn audit && yarn outdated       # yarn
```

### Before deploying to production
```bash
npm audit --audit-level=high      # npm
pnpm audit --audit-level=high     # pnpm
yarn audit                        # yarn
# Fix any high/critical vulnerabilities before deploy
```

### Lockfile discipline
- **NEVER delete** lockfiles to "fix" issues
  - `package-lock.json` (npm)
  - `pnpm-lock.yaml` (pnpm)
  - `yarn.lock` (yarn)
  - `bun.lockb` (bun)
- **ALWAYS commit** lockfile changes
- **PRODUCTION:** Use exact versions in lockfile (default behavior for all package managers)

## Dependency Assessment

### Production Dependencies

| Package | Version | Transitive Deps | Size | Verdict | Rationale |
|---------|---------|-----------------|------|---------|-----------|
| package-name | 1.2.3 | 0 | 50KB | 🟒 Keep | Zero deps, minimal, essential |
| heavy-sdk | 5.0.0 | 15 | 500KB | πŸ”΄ Watch | Consider REST API alternative |

### Dev Dependencies

| Package | Version | Purpose | Verdict |
|---------|---------|---------|---------|
| typescript | 5.9.3 | Type checking | βœ… Essential |

## Current Vulnerabilities

### [Package Name] v[version] ([severity])
- **Status:** Accepted/Mitigating/Fixing
- **Path:** package β†’ dependency β†’ vulnerable-package
- **Risk:** [Description of actual risk]
- **Mitigation:** [Why it's acceptable or what you're doing]
- **Resolution:** [Waiting for upstream/Will fix in next release/etc]

## Approved Dependency Categories

### βœ… Always Acceptable
- Official SDKs (Google Cloud, OpenAI, etc.)
- Core framework dependencies
- TypeScript types (`@types/*`)
- Build/dev tools

### ⚠️ Requires Justification
- Utility libraries (can you write it?)
- Additional ORMs/frameworks
- Alternative HTTP libraries

### ❌ Generally Avoid
- Packages with <1000 weekly downloads
- Packages not updated in >2 years
- Micro-packages for trivial tasks (left-pad syndrome)
- Multiple packages doing the same thing

## Red Flags to Watch For

If you see these, investigate immediately:
- Audit shows HIGH or CRITICAL vulnerabilities
- node_modules grows beyond reasonable size (set your threshold: 500MB, 1GB, etc.)
- Direct dependencies exceed reasonable count (set your threshold: 25, 30, 50, etc.)
- Build time increases significantly
- New deprecation warnings appear

## Monitoring Commands

### See direct dependencies only
```bash
npm ls --depth=0
pnpm ls --depth=0
yarn list --depth=0
```

### Check node_modules size
```bash
du -sh node_modules/              # Unix/Mac
dir node_modules                  # Windows (shows size in properties)
```

### Why is this package installed?
```bash
npm ls <package-name>
pnpm why <package-name>
yarn why <package-name>
```

### Find outdated packages
```bash
npm outdated
pnpm outdated
yarn outdated
```

### Check package details
```bash
npm view <package>
npm view <package> dependencies

pnpm info <package>
pnpm info <package> dependencies

yarn info <package>
```

## Adding a New Dependency - Checklist

Before running install command:

- [ ] Checked if I can write it myself in <50 lines
- [ ] Reviewed package's own dependencies
- [ ] Verified last update was <6 months ago
- [ ] Checked weekly downloads (>10k preferred)
- [ ] Measured bundle size impact (bundlephobia.com)
- [ ] Read recent issues on GitHub
- [ ] Documented reason in git commit message
- [ ] Updated this DEPENDENCIES.md if it's a new category

## Update History

### YYYY-MM-DD: [Update Description]
**Updated:**
- `package`: X.X.X β†’ Y.Y.Y
- **Compatibility:** βœ…/⚠️/❌
- **Breaking changes:** [List or "None detected"]
- **Notes:** [Any important details]

### YYYY-MM-DD: Initial Audit
**Removed X unnecessary dependencies:**
- `package-name` - Reason
- `another-package` - Reason

**Result:** XX β†’ YY dependencies (-Z%)

## Resources

- Security advisories: `pnpm audit`
- Package info: `pnpm info <package>`
- Dependency tree: `pnpm ls <package>`
- Bundle analysis: https://bundlephobia.com
- Package trends: https://npmtrends.com

Special Considerations

TypeScript Configuration

Always check that strict mode is enabled:

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

This provides your safety net - it's 80% of the safety of Go/Rust's type systems.

Common Bloat Sources to Watch For

Heavy SDKs that could be replaced:

  • Cloud storage SDKs (often can use REST API with just fetch)
  • Analytics SDKs (often can POST events directly)
  • Social media SDKs (REST APIs are usually simple)

Frameworks that pull in the world:

  • All-in-one frameworks (check what you actually use)
  • State management libraries (could you use React Context?)
  • Utility libraries (lodash, ramda - write the 3 functions you need)

Duplicate functionality:

  • Multiple HTTP clients (axios, got, node-fetch, native fetch)
  • Multiple date libraries (date-fns, moment, dayjs, native Date)
  • Multiple validation libraries (yup, joi, zod - pick one)
  • Multiple test frameworks (jest, vitest, mocha - pick one)

When to Use This Guide

Initial project setup:

  1. Run the audit process
  2. Create DEPENDENCIES.md
  3. Establish baselines and thresholds

Monthly maintenance:

  1. Run audit and outdated commands for your package manager
  2. Review any new dependencies added
  3. Update DEPENDENCIES.md with changes

Before adding new dependencies:

  1. Consult the "Before Adding a Dependency" checklist
  2. Document the decision in DEPENDENCIES.md
  3. Update the assessment table

When onboarding new developers:

  1. Have them read DEPENDENCIES.md
  2. Explain the project's dependency philosophy
  3. Use it as a code review checkpoint

Special Cases

Monorepos:

  • Run audit at root level and in each workspace
  • Watch for duplicate dependencies across workspaces
  • Use workspace protocol to share dependencies: "@my/package": "workspace:*"
  • Consider tools like manypkg check or syncpack for consistency

Frontend + Backend:

  • Maintain separate DEPENDENCIES.md for each if significantly different
  • Or use sections: "Frontend Dependencies" and "Backend Dependencies"
  • Watch for unnecessary shared dependencies

Success Metrics

A well-managed Node.js project should aim for these targets (adjust based on project type):

Backend/API Projects

  • βœ… <30 direct dependencies (total)
  • βœ… <500MB node_modules
  • βœ… 0 high/critical security vulnerabilities
  • βœ… >50% dependencies with 0 transitive deps

Frontend Projects (React/Vue/Angular)

  • βœ… <50 direct dependencies (frameworks bring more deps)
  • βœ… <1GB node_modules (build tools are larger)
  • βœ… 0 high/critical security vulnerabilities
  • βœ… >30% dependencies with 0 transitive deps

Universal Metrics

  • βœ… TypeScript strict mode enabled (if using TypeScript)
  • βœ… All dependencies documented and justified
  • βœ… Clear pinning strategy
  • βœ… Monthly audit cadence
  • βœ… No duplicate functionality
  • βœ… Lockfile committed to version control

The "Could You Do This in Go?" Test

When evaluating a JavaScript dependency, ask:

"If I were writing this in Go, would I import a library for this?"

If NO β†’ You probably don't need the JavaScript library either.

Examples:

  • String manipulation β†’ Use native methods
  • Simple HTTP requests β†’ Use fetch
  • Date formatting β†’ Use Intl.DateTimeFormat
  • Array utilities β†’ Use native map/filter/reduce
  • Retry logic β†’ Write a 10-line retry function

If YES β†’ The dependency might be justified.

Examples:

  • Database drivers (you'd use database/sql + driver)
  • HTTP routing (you'd use net/http or chi/gin)
  • Validation (you'd use a validator package)
  • ORM functionality (you might use sqlc or gorm)

Conclusion

This dependency philosophy is about conscious choices:

  • Every dependency should earn its place
  • Prefer small, focused packages over frameworks
  • Value zero-dependency packages highly
  • When in doubt, write it yourself
  • Security and maintainability over convenience

Remember: Dependencies are liabilities. Treat them like technical debt - some is necessary and good, but too much will sink the project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment