CONCEPT 12 · DESIGN
API-First Design
"Dream the call site — then work backward."
Plain English
Design the ideal user-facing interface before thinking about implementation. Ask "what would the perfect call site look like?" — the simplest, most readable way a caller would use this. Then work backward: what types does that usage demand? What laws must those types satisfy? Let the desired usage drive the design.
Analogy
An architect draws blueprints before builders pour concrete. The blueprint is the API — what the occupants need from the building. You don't start by deciding how to wire the walls or pour the foundation. You start from: "People need to cook here, sleep here, work here." The structural requirements follow from human needs, not vice versa.
You already know this
Test-Driven Development (TDD): write the test (the call site) before the implementationGraphQL schema-first: define the query API before writing resolversOpenAPI/Swagger: define the REST contract before implementing endpointsREADME-Driven Development: write the README (ideal usage docs) before writing codeUser story mapping: define what users do before designing the database schema
Code Example
JavaScript / TypeScript
// STEP 1: Write the dream call site FIRST — before any implementation
// What should running parallel tasks look like?
const [user, settings, perms] = await parallel(
fetchUser(userId),
fetchSettings(userId),
fetchPermissions(userId),
);
// Clean. No boilerplate. This is what I want users to write.
// STEP 2: What types does that usage demand?
// parallel() takes multiple Tasks and returns a Task of a tuple.
// A Task<A> must be a description of a computation producing A.
type Task<A> = () => Promise<A>;
// STEP 3: What operations must exist?
function unit<A>(a: A): Task<A> {
return () => Promise.resolve(a);
}
function map<A, B>(task: Task<A>, f: (a: A) => B): Task<B> {
return () => task().then(f);
}
function parallel<A, B, C>(
a: Task<A>, b: Task<B>, c: Task<C>
): Task<[A, B, C]> {
return () => Promise.all([a(), b(), c()]) as Promise<[A,B,C]>;
}
// STEP 4: Notice — the call site DROVE us to Promise.all.
// We didn't plan it upfront. We discovered it by asking:
// "Given this call site, what implementation makes it work?"
// API-first design makes the right implementation obvious.Apply when
▸Starting a new module, library, or service — write example usage first
▸When you're stuck on implementation — forget implementation, design the dream call site
▸Refactoring a confusing API — ask "what would ideal usage look like?" and work toward that
▸Designing internal service interfaces — start with the consumer's perspective
▸Any time your implementation is driving the API shape rather than the other way around
Check Your Understanding
You are building a retry library. What should you design first?