Back to System Design
System Design
hard
mid

What design decisions should you consider when building a library API?

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.

5 min read·~8 min to think through

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/onChange and defaultValue.
  • Headless option? — separating logic from presentation (headless UI) maximizes flexibility.
  • Escape hatchesclassName, style, ref forwarding, ...rest props 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: false so 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.

Senior engineer discussion

Seniors frame every decision around consumer DX and evolvability — small consistent surface, composition, escape hatches, types as contract, semver discipline, tree-shakability, peer deps, and docs.

Related questions