The Rise of Zero-Dependency Libraries
·PkgPulse Team
TL;DR
Zero-dependency packages are the most supply-chain-secure software you can install. No transitive vulnerabilities, no dependency conflicts, no lockfile bloat. In 2026, the best-designed libraries achieve full functionality with zero runtime dependencies — everything from state management (Zustand: 2KB, 0 deps) to ID generation (nanoid: 1KB, 0 deps) to event handling (mitt: 0.3KB, 0 deps). This is a deliberate design choice, not a constraint.
Key Takeaways
- Zero deps = zero transitive risk — the supply chain attack surface is you + your code
- Zustand, Jotai, nanoid, clsx, mitt — all zero runtime deps, all excellent
- "0 dependencies" in package.json — check
npm view package-name dependencies - Peer deps aren't the same — React as peer dep = you supply it, not the package
- The trend is accelerating: new packages launched in 2024-2026 default to zero deps
Why Zero Dependencies?
The value proposition of zero dependencies:
1. Supply chain security
→ Each dependency is a potential attack vector
→ A package with 5 deps can introduce 50+ transitive packages
→ Zero deps = only your direct install is in scope for supply chain attacks
→ leftpad incident (2016): 1 package = internet broke
→ event-stream (2018): malicious code added 5 levels deep in dep tree
2. Bundle size
→ Each dep adds to bundle size
→ Zero deps = exactly what you see on bundlephobia
→ No hidden 40KB for a utility you only use 1 function from
3. Conflict avoidance
→ Version conflicts happen when multiple packages depend on same lib
→ Zero deps eliminates this class of problem entirely
4. Predictability
→ No surprise breaking changes from dependencies you didn't update
→ Your package.json changes are your app's changes
5. Trust
→ You've audited the package; you trust it
→ With dependencies, you've audited one step; then need to trust the chain
Zero-Dependency Packages Worth Knowing
State Management
// Zustand — 2KB, 0 runtime dependencies
import { create } from 'zustand';
// React is a peer dependency (you provide it; Zustand doesn't ship it)
// The entire state management system in 2KB with no transitive deps
// Jotai — 3.1KB, 0 runtime dependencies
import { atom, useAtom } from 'jotai';
// Valtio — 2.5KB, 0 runtime dependencies
import { proxy, useSnapshot } from 'valtio';
// React is peer dep
// These replaced Redux which has:
// redux: 0 deps ✅ (surprisingly)
// react-redux: 2 deps
// @reduxjs/toolkit: 5+ deps (immer, redux-thunk, reselect, etc.)
// → RTK requires ~40KB transitive packages
Utilities
// nanoid — 1.1KB, 0 dependencies
import { nanoid } from 'nanoid';
const id = nanoid(); // 21-char URL-safe ID
// clsx — 0.5KB, 0 dependencies
import { clsx } from 'clsx';
const classes = clsx('foo', { bar: true, baz: false });
// mitt — 0.3KB, 0 dependencies
import mitt from 'mitt';
const emitter = mitt<{ event: string }>();
// ms — 0.5KB, 0 dependencies
import ms from 'ms';
ms('2 days') // 172800000
// klona — 1.1KB, 0 dependencies
import { klona } from 'klona';
const deep = klona(obj); // Deep clone, faster than JSON.parse/stringify
// bytes — 0.8KB, 0 dependencies
import bytes from 'bytes';
bytes(1024 * 1024) // '1MB'
HTTP
// In Node.js 18+: native fetch — 0 dependencies
const data = await fetch('https://api.example.com').then(r => r.json());
// No npm install needed
// undici — 0 dependencies (this is the actual Node.js HTTP impl)
import { request } from 'undici';
// Highest performance, built into Node.js core
Validation
// Valibot — designed to be tree-shakeable, 0 runtime deps
import { object, string, parse } from 'valibot';
// Each imported function is independent — zero unused code
// ArkType — 0 runtime dependencies
import { type } from 'arktype';
const user = type({ name: 'string', age: 'number' });
The Peer Dependency Distinction
# Zero dependencies ≠ no peer dependencies
# These are different:
# Dependencies (ships WITH the package, adds to your node_modules):
# npm view lodash dependencies → {} (zero deps — good)
# npm view react-router-dom dependencies → { react, @remix-run/... } (ships deps)
# Peer dependencies (YOU provide these, NOT shipped by the package):
# npm view zustand peerDependencies → { react: ">= 16.8" }
# This means: "zustand REQUIRES react but expects YOU to install it"
# Zustand itself has zero runtime dependencies
# How to check:
npm view package-name dependencies
# Empty object → zero runtime deps
npm view package-name peerDependencies
# Lists what you must provide but package doesn't ship
# The ideal pattern:
# peerDependencies: { react: "*" } ← you control the react version
# dependencies: {} ← zero hidden packages
# devDependencies: { react, typescript, ... } ← just for their tests
How Well-Designed Libraries Achieve Zero Deps
// Technique 1: Use Web Platform APIs instead of utility packages
// Instead of: npm install uuid
const id = crypto.randomUUID(); // Web Crypto API, built into Node 18+
// Instead of: npm install node-fetch
const data = await fetch(url).then(r => r.json()); // Built-in Node 18+
// Instead of: npm install deep-equal
const equal = JSON.stringify(a) === JSON.stringify(b); // For JSON-safe objects
// Or: structuredClone comparison (Node 17+)
// Technique 2: Tiny implementation instead of heavy library
// Zustand's entire core (~200 lines of TypeScript):
// - Uses React.useSyncExternalStore (built-in React 18)
// - Subscription model using a Set (built-in JS)
// - No immer, no redux patterns, no middleware by default
// Result: complete state management in 200 lines, 0 deps
// Technique 3: Bundling micro-dependencies they control
// dayjs bundles all locales as optional plugin files
// No external dependency on a locale data package
// You get what you import, nothing extra
// Technique 4: Being opinionated about scope
// A good library does one thing and doesn't need 10 utilities to do it
// Scope creep requires dependencies; focused scope doesn't
The Math: How Dependencies Multiply
# Check how many packages a single dependency brings:
npm install --dry-run express 2>&1 | tail -3
# added 57 packages from 44 contributors
npm install --dry-run fastify 2>&1 | tail -3
# added 8 packages
npm install --dry-run zustand 2>&1 | tail -3
# added 1 package ← just zustand itself
# The difference:
# express: 57 packages to audit, 57 potential attack vectors
# fastify: 8 packages
# zustand: 1 package
# At project scale, this compounds:
# A project with 20 direct deps might have 200-500 transitive deps
# Every one is in your supply chain
# Zero-dep packages: the "1 package" count is real
# They don't multiply
Zero-Dep Alternatives for Common Tasks
| Task | With Dependencies | Zero-Dep Alternative |
|---|---|---|
| Unique IDs | uuid (14KB, 1 dep) | crypto.randomUUID() built-in |
| Deep clone | lodash.clonedeep (5KB) | structuredClone() built-in |
| Directory creation | mkdirp (0.5KB, 1 dep) | fs.mkdirSync(path, {recursive:true}) |
| Recursive delete | rimraf (1KB, 1 dep) | fs.rmSync(path, {recursive:true, force:true}) |
| HTTP fetch | node-fetch (0.5KB) | fetch() built-in Node 18+ |
| Event emitter | eventemitter3 (2KB, 0 deps) | mitt (0.3KB, 0 deps) |
| String colors | chalk (1.5KB, varies) | picocolors (0.3KB, 0 deps) |
| State management | Redux (10KB+) | Zustand (2KB, 0 deps) |
| Date formatting | moment (72KB) | dayjs (2.7KB, 0 deps) |
The Zero-Dep Checklist
# Before installing any package, verify:
# 1. Check runtime deps:
npm view package-name dependencies
# Empty? → Zero deps ✅
# 2. Verify it's not cheating (bundling huge libs):
npx bundlephobia-cli package-name
# If zero deps but still 50KB+: it bundled something
# 3. Check peer deps (fine — you control these):
npm view package-name peerDependencies
# 4. Ask: is there a built-in alternative?
# Node.js 18-22 added: fetch, crypto.randomUUID, structuredClone,
# fs.rm, fs.mkdir recursive, Web Streams API, AbortController
# Modern browsers: same APIs
# Check MDN/Node.js docs before npm installing anything
# 5. For utilities you do need: prefer the zero-dep option
# Same functionality, less risk, smaller bundle
Compare bundle sizes and dependency counts for npm packages at PkgPulse.
See the live comparison
View date fns vs. dayjs on PkgPulse →