Frontend Architecture
Part 0 of 11The Hidden Cost of "Just Add a Library"
The Hidden Cost of "Just Add a Library"
How dependency decisions compound over time, bundle bloat, security surface area, and building a team policy around third-party packages
The Moment of Weakness
It's 4 PM on a Thursday. You need to format some dates. You could write the formatting function yourself—maybe 20 lines of code. Or you could npm install moment and be done in 30 seconds.
You install the library.
Three years later, your bundle is 2.4 MB, you have 1,847 dependencies, and you just got paged because a transitive dependency of a transitive dependency had a critical vulnerability. The security team wants to know why you're shipping is-odd (which depends on is-number) to check if a number is odd.
This is the story of how "just add a library" compounds into technical debt that's invisible until it's crushing.
The True Cost of a Dependency
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE DEPENDENCY ICEBERG │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ What you see: │
│ ┌─────────────────────┐ │
│ │ npm install lodash │ │
│ │ "It works!" │ │
│ └─────────────────────┘ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ What you don't see: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Bundle Size Impact │ │
│ │ • Full lodash: 71 KB minified │ │
│ │ • You used: _.debounce (500 bytes) │ │
│ │ • 99.3% of the code is unused │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Security Surface │ │
│ │ • Another package to monitor for CVEs │ │
│ │ • Transitive dependencies to audit │ │
│ │ • Supply chain attack vector │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Maintenance Burden │ │
│ │ • Breaking changes in major versions │ │
│ │ • Compatibility with other dependencies │ │
│ │ • TypeScript type updates │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Knowledge Fragmentation │ │
│ │ • New team members must learn library API │ │
│ │ • Different developers use different libs for same thing │ │
│ │ • Debugging requires understanding library internals │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ License Compliance │ │
│ │ • Is it MIT? Apache? GPL? │ │
│ │ • Transitive license conflicts? │ │
│ │ • Legal review required? │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The Compounding Effect
Year One vs Year Five
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEPENDENCY GROWTH OVER TIME │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Year 1 (Startup Mode): │
│ "Move fast, we'll clean it up later" │
│ │
│ package.json: │
│ ├── 45 direct dependencies │
│ ├── 312 total (with transitive) │
│ ├── Bundle: 380 KB │
│ └── npm audit: 2 low, 0 moderate, 0 high │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Year 3 (Growth Mode): │
│ "We need to ship features, not refactor dependencies" │
│ │
│ package.json: │
│ ├── 127 direct dependencies │
│ ├── 1,284 total (with transitive) │
│ ├── Bundle: 1.2 MB │
│ ├── npm audit: 12 low, 8 moderate, 2 high │
│ └── 3 deprecated packages │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ Year 5 (Maintenance Mode): │
│ "We can't upgrade React because of dependency conflicts" │
│ │
│ package.json: │
│ ├── 203 direct dependencies │
│ ├── 2,847 total (with transitive) │
│ ├── Bundle: 2.8 MB │
│ ├── npm audit: 34 low, 18 moderate, 7 high, 2 critical │
│ ├── 12 deprecated packages │
│ ├── 5 packages with no maintainer │
│ └── Node.js upgrade blocked by native dependency │
│ │
│ The "clean it up later" moment never came. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Real Cost Analysis
// Let's calculate the actual cost of a "simple" dependency decision
interface DependencyCost {
bundleSize: number; // KB added to bundle
installTime: number; // Seconds added to npm install
dependencyCount: number; // Transitive dependencies added
weeklyDownloads: number; // Indicator of maintenance likelihood
lastPublish: Date; // Freshness
securityIssues: number; // Known vulnerabilities
maintenanceHours: number; // Annual hours for updates
}
// Example: Adding a date formatting library
const dateLibraryOptions: Record<string, DependencyCost> = {
'moment': {
bundleSize: 71, // KB minified
installTime: 2.3,
dependencyCount: 0, // No deps, but huge
weeklyDownloads: 15_000_000,
lastPublish: new Date('2022-10-06'), // In maintenance mode
securityIssues: 0,
maintenanceHours: 4 // Dealing with deprecation warnings
},
'date-fns': {
bundleSize: 13, // With tree-shaking
installTime: 1.8,
dependencyCount: 0,
weeklyDownloads: 20_000_000,
lastPublish: new Date('2024-01-15'),
securityIssues: 0,
maintenanceHours: 2
},
'dayjs': {
bundleSize: 2.9,
installTime: 0.8,
dependencyCount: 0,
weeklyDownloads: 14_000_000,
lastPublish: new Date('2024-01-10'),
securityIssues: 0,
maintenanceHours: 1
},
'native Intl API': {
bundleSize: 0, // Built into browser/Node
installTime: 0,
dependencyCount: 0,
weeklyDownloads: Infinity, // Always available
lastPublish: new Date(), // Always current
securityIssues: 0,
maintenanceHours: 0
}
};
// For simple date formatting, the native solution is often sufficient:
function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
}
// The "cost" of writing this yourself: 5 minutes, 0 dependencies
Bundle Bloat: Death by a Thousand Imports
Analyzing Your Bundle
// webpack-bundle-analyzer or @next/bundle-analyzer reveals the truth
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// your config
});
// Run: ANALYZE=true npm run build
Common Bloat Patterns
┌─────────────────────────────────────────────────────────────────────────────┐
│ BUNDLE BLOAT PATTERNS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Pattern 1: The Kitchen Sink Import │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ❌ import _ from 'lodash'; // 71 KB │ │
│ │ const result = _.debounce(fn, 300); │ │
│ │ │ │
│ │ ✅ import debounce from 'lodash/debounce'; // 2 KB │ │
│ │ const result = debounce(fn, 300); │ │
│ │ │ │
│ │ ✅✅ function debounce(fn, ms) { // 0 KB (10 lines) │ │
│ │ let timeout; │ │
│ │ return (...args) => { │ │
│ │ clearTimeout(timeout); │ │
│ │ timeout = setTimeout(() => fn(...args), ms); │ │
│ │ }; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Pattern 2: The Unnecessary Polyfill │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ❌ import 'core-js/stable'; // 150+ KB │ │
│ │ "Just in case we need IE11 support" │ │
│ │ │ │
│ │ ✅ // Check browserslist, target modern browsers │ │
│ │ // Array.includes, Promise, fetch - all native now │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Pattern 3: The Framework Overkill │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ❌ npm install @material-ui/core // 300+ KB │ │
│ │ // Used: Button, TextField (could be 5 KB custom) │ │
│ │ │ │
│ │ ✅ Build simple components yourself │ │
│ │ ✅ Use lighter alternatives (Radix, Headless UI) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Pattern 4: The Icon Library Trap │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ❌ import { FaHome } from 'react-icons/fa'; │ │
│ │ // Imports entire icon set (1000+ icons) │ │
│ │ │ │
│ │ ✅ import FaHome from 'react-icons/fa/FaHome'; │ │
│ │ ✅ Use SVG directly (0 JS overhead) │ │
│ │ ✅ Use SVG sprites │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Pattern 5: The Dev Dependency in Prod │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ❌ import debug from 'debug'; // Accidentally in bundle │ │
│ │ ❌ import faker from '@faker-js/faker'; // Test data in prod │ │
│ │ │ │
│ │ ✅ Proper code splitting │ │
│ │ ✅ Tree-shaking + dead code elimination │ │
│ │ ✅ Bundle analysis in CI │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Bundle Budget Enforcement
// scripts/check-bundle-size.ts
import { execSync } from 'child_process';
import fs from 'fs';
interface BundleBudget {
path: string;
maxSize: number; // KB
}
const budgets: BundleBudget[] = [
{ path: '.next/static/chunks/main-*.js', maxSize: 100 },
{ path: '.next/static/chunks/pages/_app-*.js', maxSize: 150 },
{ path: '.next/static/chunks/framework-*.js', maxSize: 50 },
// Total JS budget
{ path: '.next/static/chunks/**/*.js', maxSize: 400 },
];
function checkBudgets() {
const violations: string[] = [];
for (const budget of budgets) {
const files = glob.sync(budget.path);
const totalSize = files.reduce((sum, file) => {
const stats = fs.statSync(file);
return sum + stats.size / 1024; // KB
}, 0);
if (totalSize > budget.maxSize) {
violations.push(
`${budget.path}: ${totalSize.toFixed(1)}KB > ${budget.maxSize}KB`
);
}
}
if (violations.length > 0) {
console.error('Bundle budget violations:');
violations.forEach(v => console.error(` - ${v}`));
process.exit(1);
}
console.log('All bundle budgets passed!');
}
checkBudgets();
# .github/workflows/ci.yml
- name: Build
run: npm run build
- name: Check bundle size
run: npm run check-bundle
- name: Upload bundle analysis
uses: actions/upload-artifact@v4
with:
name: bundle-analysis
path: .next/analyze/
Security Surface Area
The Supply Chain Problem
┌─────────────────────────────────────────────────────────────────────────────┐
│ SUPPLY CHAIN ATTACK SURFACE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Your App │
│ │ │
│ ├── express (trusted, 50M weekly downloads) │
│ │ └── body-parser │
│ │ └── raw-body │
│ │ └── unpipe │
│ │ └── ??? (who audits this?) │
│ │ │
│ ├── some-useful-package │
│ │ └── another-package │
│ │ └── yet-another │
│ │ └── event-stream ← COMPROMISED (2018 attack) │
│ │ └── flatmap-stream ← Malicious code │
│ │ │
│ └── convenient-utility (12 weekly downloads, 1 maintainer) │
│ └── ??? (do you trust this person?) │
│ │
│ Key Stats: │
│ • Average npm project: 79 transitive dependencies │
│ • Real-world project: Often 500-2000+ dependencies │
│ • You personally audit: Maybe 5-10 │
│ • Attack surface: Every single one │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Notable Supply Chain Attacks
┌─────────────────────────────────────────────────────────────────────────────┐
│ SUPPLY CHAIN ATTACKS (REAL EXAMPLES) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ event-stream (2018) │
│ • 2M weekly downloads │
│ • Maintainer gave access to "helpful" contributor │
│ • Malicious code added targeting cryptocurrency wallets │
│ • Went undetected for 2 months │
│ │
│ ua-parser-js (2021) │
│ • 7M weekly downloads │
│ • NPM account compromised │
│ • Crypto miner + password stealer injected │
│ • Affected for ~4 hours │
│ │
│ colors + faker (2022) │
│ • Maintainer intentionally corrupted own packages │
│ • Protest against open source exploitation │
│ • Broke thousands of projects │
│ │
│ node-ipc (2022) │
│ • Maintainer added "protestware" │
│ • Deleted files on Russian/Belarusian IP addresses │
│ • Affected vue-cli and other major projects │
│ │
│ Lessons: │
│ • Popular packages are targets │
│ • Single maintainers are a risk │
│ • Even "trusted" packages can be compromised │
│ • Speed of npm install = speed of propagation │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Defensive Measures
// 1. Lock file integrity - always commit package-lock.json
// package.json
{
"scripts": {
"preinstall": "npx only-allow npm", // Prevent mixing package managers
"postinstall": "npm audit --audit-level=high"
}
}
// 2. Use npm audit in CI
// .github/workflows/security.yml
name: Security Audit
on:
push:
schedule:
- cron: '0 0 * * *' # Daily
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Security audit
run: npm audit --audit-level=high
- name: Check for known vulnerabilities
run: npx better-npm-audit audit
- name: License check
run: npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC'
// 3. Dependency review automation
// scripts/review-new-deps.ts
interface DependencyRisk {
name: string;
version: string;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
reasons: string[];
}
async function assessDependency(name: string): Promise<DependencyRisk> {
const npmInfo = await fetch(`https://registry.npmjs.org/${name}`).then(r => r.json());
const reasons: string[] = [];
let riskLevel: DependencyRisk['riskLevel'] = 'low';
// Check maintainer count
const maintainers = npmInfo.maintainers?.length || 0;
if (maintainers === 1) {
reasons.push('Single maintainer (bus factor = 1)');
riskLevel = 'medium';
}
// Check last publish date
const lastPublish = new Date(npmInfo.time?.modified);
const monthsAgo = (Date.now() - lastPublish.getTime()) / (1000 * 60 * 60 * 24 * 30);
if (monthsAgo > 12) {
reasons.push(`No updates in ${Math.floor(monthsAgo)} months`);
riskLevel = 'medium';
}
if (monthsAgo > 24) {
riskLevel = 'high';
}
// Check weekly downloads
const downloads = await fetch(
`https://api.npmjs.org/downloads/point/last-week/${name}`
).then(r => r.json());
if (downloads.downloads < 1000) {
reasons.push(`Low usage: ${downloads.downloads} weekly downloads`);
riskLevel = riskLevel === 'low' ? 'medium' : 'high';
}
// Check for known vulnerabilities
const audit = await checkVulnerabilities(name);
if (audit.vulnerabilities > 0) {
reasons.push(`${audit.vulnerabilities} known vulnerabilities`);
riskLevel = 'critical';
}
// Check dependency count
const deps = Object.keys(npmInfo.versions?.[npmInfo['dist-tags']?.latest]?.dependencies || {});
if (deps.length > 20) {
reasons.push(`High dependency count: ${deps.length} dependencies`);
}
return {
name,
version: npmInfo['dist-tags']?.latest,
riskLevel,
reasons
};
}
// Run before npm install of new packages
async function reviewNewDependencies() {
const lockBefore = readLockfile('package-lock.json.backup');
const lockAfter = readLockfile('package-lock.json');
const newDeps = findNewDependencies(lockBefore, lockAfter);
console.log(`Reviewing ${newDeps.length} new dependencies...`);
for (const dep of newDeps) {
const risk = await assessDependency(dep);
console.log(`\n${dep}: ${risk.riskLevel.toUpperCase()}`);
risk.reasons.forEach(r => console.log(` - ${r}`));
if (risk.riskLevel === 'critical') {
console.error(`\n❌ CRITICAL: ${dep} should not be added without security review`);
process.exit(1);
}
if (risk.riskLevel === 'high') {
console.warn(`\n⚠️ HIGH RISK: ${dep} requires justification`);
}
}
}
Building a Team Policy
The Dependency Decision Framework
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEPENDENCY DECISION FLOWCHART │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ "I want to add a dependency" │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Can you do it with │ │
│ │ native JS/platform? │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ Yes Maybe No │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Do that. Is it < 50 LOC Continue │
│ Stop here. to write yourself? │ │
│ │ │ │
│ ┌────────────────┼───────┐ │ │
│ ▼ ▼ ▼ │ │
│ Yes No Complex │ │
│ │ │ │ │ │
│ ▼ │ │ │ │
│ Write it. │ ▼ │ │
│ Stop here. │ Consider │ │
│ │ library │ │
│ │ │ │ │
│ └───────┼────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Does an approved library │ │
│ │ already exist in codebase? │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ Yes No Partial │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Use that. Evaluate options Can existing │
│ Stop here. │ lib be extended? │
│ │ │ │
│ ▼ Yes──┼──No │
│ ┌─────────────────────────┐ │ │ │
│ │ Check against criteria: │ │ │ │
│ │ • > 10K weekly downloads│ │ │ │
│ │ • Multiple maintainers │ │ │ │
│ │ • Recent updates │ │ │ │
│ │ • Acceptable license │ │ │ │
│ │ • < 10 dependencies │ │ │ │
│ │ • Passes security audit │◄───┘ │ │
│ └──────────────┬──────────┘ │ │
│ │ │ │
│ ┌─────────┼─────────┐ │ │
│ ▼ ▼ ▼ ▼ │
│ Pass Mixed Fail Evaluate │
│ │ │ │ new lib │
│ ▼ ▼ ▼ │
│ Add it. Team review Find │
│ required. alternative. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The Approved Dependencies List
// docs/approved-dependencies.ts
// This file documents our approved dependencies and their use cases
export const approvedDependencies = {
// Date/Time
dateTime: {
approved: ['date-fns'],
rejected: ['moment'], // Deprecated, large bundle
reason: 'date-fns is tree-shakeable, actively maintained, smaller',
nativeAlternative: 'Use Intl.DateTimeFormat for simple formatting'
},
// HTTP Client
httpClient: {
approved: ['native fetch', 'ky'],
rejected: ['axios', 'superagent'],
reason: 'fetch is native, ky is a tiny wrapper when needed',
nativeAlternative: 'fetch() covers 95% of use cases'
},
// Validation
validation: {
approved: ['zod'],
rejected: ['yup', 'joi'],
reason: 'zod has better TypeScript inference, smaller bundle',
nativeAlternative: null
},
// State Management
stateManagement: {
approved: ['zustand', '@tanstack/react-query'],
rejected: ['redux', 'mobx'],
reason: 'Simpler API, smaller bundle, covers most use cases',
nativeAlternative: 'React Context + useReducer for simple cases'
},
// Styling
styling: {
approved: ['tailwindcss', 'clsx'],
rejected: ['styled-components', 'emotion'],
reason: 'Tailwind has zero runtime cost, better performance',
nativeAlternative: 'CSS Modules for component-scoped styles'
},
// Forms
forms: {
approved: ['react-hook-form'],
rejected: ['formik'],
reason: 'Better performance, smaller bundle, hooks-based',
nativeAlternative: 'Native forms for simple cases'
},
// Testing
testing: {
approved: ['vitest', '@testing-library/react', 'playwright'],
rejected: ['jest', 'enzyme', 'cypress'],
reason: 'vitest is faster, enzyme is deprecated',
nativeAlternative: null
},
// Utilities
utilities: {
approved: [], // Write your own utilities
rejected: ['lodash', 'underscore', 'ramda'],
reason: 'Most lodash functions are simple to write, bloat bundle',
nativeAlternative: 'See utilities.ts for our implementations'
}
};
// Document reasons for rejections
export const rejectionReasons = {
'moment': 'Deprecated by maintainers, 71KB bundle size, mutable API',
'axios': 'fetch() is native, axios adds 14KB for minimal benefit',
'lodash': 'Tree-shaking is unreliable, most functions are trivial',
'redux': 'Excessive boilerplate, zustand covers same use cases simpler',
'styled-components': '12KB runtime, performance overhead, CSS-in-JS issues',
'enzyme': 'Officially deprecated, testing-library is the standard',
};
Writing Your Own Utilities
// lib/utils.ts
// Common utilities we write ourselves instead of importing libraries
// Debounce (instead of lodash.debounce)
export function debounce<T extends (...args: any[]) => any>(
fn: T,
ms: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}
// Throttle (instead of lodash.throttle)
export function throttle<T extends (...args: any[]) => any>(
fn: T,
ms: number
): (...args: Parameters<T>) => void {
let lastCall = 0;
return (...args: Parameters<T>) => {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
fn(...args);
}
};
}
// Deep clone (instead of lodash.cloneDeep)
export function deepClone<T>(obj: T): T {
return structuredClone(obj); // Native in modern browsers/Node
}
// Deep equal (instead of lodash.isEqual)
export function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (a === null || b === null) return false;
if (typeof a !== 'object') return false;
const aObj = a as Record<string, unknown>;
const bObj = b as Record<string, unknown>;
const aKeys = Object.keys(aObj);
const bKeys = Object.keys(bObj);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every(key => deepEqual(aObj[key], bObj[key]));
}
// Group by (instead of lodash.groupBy)
export function groupBy<T>(
array: T[],
keyFn: (item: T) => string
): Record<string, T[]> {
return array.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] ??= []).push(item);
return acc;
}, {} as Record<string, T[]>);
}
// Pick (instead of lodash.pick)
export function pick<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
return keys.reduce((acc, key) => {
if (key in obj) acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
}
// Omit (instead of lodash.omit)
export function omit<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): Omit<T, K> {
const result = { ...obj };
keys.forEach(key => delete result[key]);
return result as Omit<T, K>;
}
// Unique (instead of lodash.uniq)
export function unique<T>(array: T[]): T[] {
return [...new Set(array)];
}
// Unique by key (instead of lodash.uniqBy)
export function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[] {
const seen = new Set();
return array.filter(item => {
const key = keyFn(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
// Chunk (instead of lodash.chunk)
export function chunk<T>(array: T[], size: number): T[][] {
return Array.from(
{ length: Math.ceil(array.length / size) },
(_, i) => array.slice(i * size, i * size + size)
);
}
// Flatten (native in modern JS)
// array.flat() and array.flatMap() are native
// Random (instead of lodash.random)
export function random(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Sleep (instead of a library)
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Format number (instead of numeral.js)
export function formatNumber(n: number, locale = 'en-US'): string {
return new Intl.NumberFormat(locale).format(n);
}
// Format currency (instead of accounting.js)
export function formatCurrency(
amount: number,
currency = 'USD',
locale = 'en-US'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
// Format relative time (instead of moment.fromNow())
export function formatRelativeTime(date: Date, locale = 'en-US'): string {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diff = date.getTime() - Date.now();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (Math.abs(days) >= 1) return rtf.format(days, 'day');
if (Math.abs(hours) >= 1) return rtf.format(hours, 'hour');
if (Math.abs(minutes) >= 1) return rtf.format(minutes, 'minute');
return rtf.format(seconds, 'second');
}
Dependency Hygiene
Regular Maintenance Schedule
// scripts/dependency-maintenance.ts
interface MaintenanceTask {
task: string;
frequency: 'daily' | 'weekly' | 'monthly' | 'quarterly';
automated: boolean;
command?: string;
}
const maintenanceTasks: MaintenanceTask[] = [
{
task: 'Security audit',
frequency: 'daily',
automated: true,
command: 'npm audit --audit-level=high'
},
{
task: 'Check for outdated packages',
frequency: 'weekly',
automated: true,
command: 'npm outdated'
},
{
task: 'Update patch versions',
frequency: 'weekly',
automated: true,
command: 'npm update'
},
{
task: 'Review and update minor versions',
frequency: 'monthly',
automated: false,
command: 'npx npm-check-updates -u --target minor'
},
{
task: 'Review major version updates',
frequency: 'monthly',
automated: false,
command: 'npx npm-check-updates'
},
{
task: 'Audit unused dependencies',
frequency: 'monthly',
automated: true,
command: 'npx depcheck'
},
{
task: 'Review dependency licenses',
frequency: 'quarterly',
automated: true,
command: 'npx license-checker --summary'
},
{
task: 'Full dependency review',
frequency: 'quarterly',
automated: false
}
];
Automated Dependency Updates
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
# Group related updates
groups:
typescript:
patterns:
- "typescript"
- "@types/*"
testing:
patterns:
- "vitest"
- "@testing-library/*"
- "playwright"
linting:
patterns:
- "eslint*"
- "prettier"
- "@typescript-eslint/*"
# Limit open PRs
open-pull-requests-limit: 10
# Ignore major versions (review manually)
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
# Add labels for filtering
labels:
- "dependencies"
- "automated"
# Reviewers for dependency PRs
reviewers:
- "team-platform"
# .github/workflows/dependency-review.yml
name: Dependency Review
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC
- name: Check bundle impact
run: |
npm ci
npm run build
npm run check-bundle
- name: Report new dependencies
run: |
node scripts/report-new-deps.js >> $GITHUB_STEP_SUMMARY
Removing Unused Dependencies
// scripts/find-unused-deps.ts
import { execSync } from 'child_process';
async function findUnusedDependencies() {
console.log('Checking for unused dependencies...\n');
// Use depcheck
const result = execSync('npx depcheck --json', { encoding: 'utf-8' });
const report = JSON.parse(result);
if (report.dependencies.length > 0) {
console.log('Unused dependencies:');
report.dependencies.forEach((dep: string) => {
console.log(` - ${dep}`);
});
console.log(`\nRemove with: npm uninstall ${report.dependencies.join(' ')}`);
}
if (report.devDependencies.length > 0) {
console.log('\nUnused devDependencies:');
report.devDependencies.forEach((dep: string) => {
console.log(` - ${dep}`);
});
console.log(`\nRemove with: npm uninstall -D ${report.devDependencies.join(' ')}`);
}
if (report.missing && Object.keys(report.missing).length > 0) {
console.log('\n⚠️ Missing dependencies (imported but not in package.json):');
Object.entries(report.missing).forEach(([dep, files]) => {
console.log(` - ${dep}`);
(files as string[]).forEach(f => console.log(` used in: ${f}`));
});
}
// Check for duplicate packages
console.log('\nChecking for duplicate packages...');
const dedupe = execSync('npm dedupe --dry-run 2>&1 || true', { encoding: 'utf-8' });
if (dedupe.includes('removed')) {
console.log('Duplicates found! Run: npm dedupe');
} else {
console.log('No duplicates found.');
}
}
findUnusedDependencies();
The Dependency Review Checklist
## Before Adding a New Dependency
### Is It Necessary?
□ Can this be done with native JavaScript/platform APIs?
□ Is the functionality simple enough to implement ourselves (< 50 LOC)?
□ Do we already have an approved library that covers this use case?
□ Is this solving a real problem or a perceived problem?
### Package Health
□ Weekly downloads > 10,000 (unless niche/specialized)
□ Multiple maintainers (bus factor > 1)
□ Last publish within 6 months
□ No known security vulnerabilities
□ Active issue response (issues being addressed)
□ TypeScript types available (or @types package)
### Size and Performance
□ Bundle size impact analyzed
□ Tree-shaking supported
□ No unnecessary transitive dependencies
□ Within our bundle budget
### Security
□ npm audit passes
□ Maintainers have 2FA enabled (check npm)
□ No history of security incidents
□ License is compatible (MIT, Apache-2.0, BSD, ISC)
### Long-term Viability
□ Not deprecated or in maintenance mode
□ Not a single-person hobby project (unless simple)
□ Clear migration path if abandoned
□ We could fork and maintain if needed
### Team Alignment
□ Discussed with team lead / architect
□ Added to approved dependencies list
□ Documentation updated
□ Team knows why this was chosen over alternatives
Quick Reference
Do You Really Need That Library?
┌─────────────────────────────────────────────────────────────────────────────┐
│ LIBRARY NECESSITY CHECK │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Instead of... Use native/simple... │
│ ───────────────────────────────────────────────────────────────────── │
│ lodash.get Optional chaining: obj?.a?.b?.c │
│ lodash.defaultsDeep Spread: { ...defaults, ...obj } │
│ lodash.debounce 10-line implementation │
│ lodash.cloneDeep structuredClone(obj) │
│ lodash.isEqual JSON.stringify or custom │
│ lodash.uniq [...new Set(array)] │
│ lodash.flatten array.flat() │
│ lodash.groupBy Object.groupBy() or reduce │
│ │
│ moment / date-fns Intl.DateTimeFormat, Intl.RelativeTimeFormat │
│ numeral.js Intl.NumberFormat │
│ uuid crypto.randomUUID() │
│ classnames / clsx Template literal: `btn ${active && 'active'}` │
│ axios fetch() │
│ qs URLSearchParams │
│ isomorphic-fetch fetch() is native now │
│ left-pad String.prototype.padStart() │
│ is-odd n % 2 !== 0 │
│ is-number typeof n === 'number' && !isNaN(n) │
│ │
│ Rule of thumb: If you can explain the implementation in one sentence, │
│ you probably don't need a library. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Dependency Red Flags
🚩 Single maintainer with no recent activity
🚩 No TypeScript support (in 2024+)
🚩 Last publish > 12 months ago
🚩 Weekly downloads < 1,000 (unless specialized)
🚩 More dependencies than your entire app
🚩 "Fixes" node_modules or uses postinstall scripts
🚩 Minified source code in repository
🚩 Maintainer asking for donations aggressively
🚩 Forked 500 times with no clear "main" fork
🚩 README says "This project is no longer maintained"
Closing Thoughts
Every dependency is a bet. You're betting that the maintainers will:
- Keep it secure
- Keep it compatible with your stack
- Not abandon it
- Not have a breakdown and delete everything
Some bets are safe. React, TypeScript, PostgreSQL—these have organizations behind them. But convenient-date-formatter with 47 weekly downloads? That's a coin flip.
The "just add a library" mentality is seductive because it offers instant gratification. The cost is invisible until it isn't. Until you're three hours into debugging why a transitive dependency of a dependency broke your production build. Until you're explaining to security why you're shipping code you've never audited.
The solution isn't to write everything from scratch—that's equally foolish. The solution is to be intentional:
- Default to native when possible
- Write simple utilities yourself
- Choose dependencies deliberately with clear criteria
- Maintain them actively with regular audits
- Remove them aggressively when they're no longer needed
Every dependency should earn its place in your package.json. The ones that don't should be shown the door.
Your bundle size, security posture, and long-term maintainability will thank you. So will the developers who inherit your codebase in five years.
What did you think?