Library API design decisions to consider
Key decisions: a small, intuitive surface; sensible defaults with progressive disclosure; consistency and naming; controlled vs uncontrolled; composition over configuration; TypeScript types as the contract; semver discipline; tree-shakability; minimal peer deps; good errors; and docs. Optimize for the consumer, and for change.
Designing a library API means designing for people you'll never meet, and for change over time. The decisions:
Surface & ergonomics
- Small surface area — expose the minimum that's useful. Every public API is a forever-commitment.
- Sensible defaults, progressive disclosure — the common case should need almost no config; advanced power available but not in your face.
- Consistency — naming, argument order, return shapes, async patterns all uniform. Predictability beats cleverness.
- Intuitive naming — names that reveal intent; follow ecosystem conventions.
Architecture
- Composition over configuration — small composable pieces beat one giant component with 40 props. (Compound components, hooks, plugins.)
- Controlled vs uncontrolled — for UI components, support both:
value/onChangeanddefaultValue. - Headless option? — separating logic from presentation (headless UI) maximizes flexibility.
- Escape hatches —
className,style,refforwarding,...restprops so consumers aren't trapped.
The contract
- TypeScript types are the API contract — precise, exported types; they're documentation and a compile-time guard.
- Good error messages — actionable, pointing at the fix, with dev-only warnings for misuse.
- Predictable, documented side effects — be explicit about what the library touches.
Versioning & change
- Semver discipline — breaking changes are major versions, period. Deprecate with warnings before removing.
- Backwards compatibility — additive changes preferred; migration guides + codemods for breaks.
Distribution & footprint
- Tree-shakable — ESM build, named exports,
sideEffects: falseso consumers ship only what they use. - Ship ESM + CJS (and types).
- Minimal dependencies; use peerDependencies for shared libs (React) so consumers control the version and you don't duplicate it.
- Bundle size is a feature — measure and budget it.
Beyond code
- Documentation & examples — the API isn't usable if it isn't documented; runnable examples.
- Accessibility baked in for UI libraries.
The overarching principle
Optimize for the consumer's experience and for your ability to evolve it without breaking them. Those two — DX and changeability — drive most of the decisions.
The framing
"You're designing for strangers and for change. So: a small, consistent surface with sensible defaults and progressive disclosure; composition over a mega-config component; controlled and uncontrolled support; escape hatches like className and ref forwarding. TypeScript types are the contract. Then the change axis — strict semver, deprecate-before-remove, migration guides. And distribution — tree-shakable ESM + CJS, minimal deps with peerDependencies for shared libs, bundle size as a budgeted feature. Plus docs and accessibility. The two things every decision serves are consumer DX and your freedom to evolve it without breaking them."
Follow-up questions
- •Why prefer composition over a heavily-configurable component?
- •Why are TypeScript types the API contract?
- •How does semver discipline affect API design?
- •Why use peerDependencies for something like React?
Common mistakes
- •Huge API surface — everything is now a forever-commitment.
- •A mega-component with dozens of props instead of composition.
- •Breaking changes in minor versions.
- •No escape hatches — consumers get trapped.
- •Not tree-shakable; bundling React instead of peer-depending on it.
Performance considerations
- •Bundle size is a first-class API concern — tree-shakability, minimal deps, and peerDependencies keep the consumer's bundle small. Lazy/optional sub-modules let consumers pay only for what they use.
Edge cases
- •A consumer needs behavior you didn't anticipate — escape hatches.
- •Supporting multiple major versions of a peer dependency.
- •Deprecating an API still widely used.
- •SSR compatibility.
Real-world examples
- •Radix UI / Headless UI — composition, controlled/uncontrolled, headless, accessible.
- •React itself shipped as a peer dependency for the ecosystem.