API Design Case Studies
Complete worked contracts: consumers → requirements → resources → operations → errors → decision log → evolution. Each decision names its alternative and its trade-off, because a design without a rejected alternative is a guess.
This is the study to internalize before any of the others, because it shows the full path from requirements to operations with nothing exotic in the way. The domain sounds like CRUD — users, projects — but the moment you write down what people actually do (**invite someone who has no account yet**, **change a member's role**, **leave a project you don't own**), plain CRUD stops covering it. The design work is deciding which of those verbs deserve their own resources ([[resource-modeling]]), which are operations on relationships, and which are just field updates. Follow the reasoning, not the endpoint shapes: the shapes fall out once the resources are right.
Payments are where every abstract API-design lesson turns into money. The network *will* drop a response after the charge succeeded; the client *will* retry; the card network *will* take 4 seconds sometimes and time out other times; and a webhook *will* arrive twice, or late, or never. The design answer is not heroic infrastructure — it's a contract built from four ideas: **required idempotency keys** on anything that moves money ([[idempotency-keys]]), an **explicit state machine** instead of a mutable `status` string ([[api-state-machines]]), **errors that separate "declined" from "broken"** ([[error-taxonomy]]), and **reconciliation as a first-class read path** so no consumer ever has to trust a webhook as the source of truth. Every decision below exists because one of those failure modes is otherwise a double charge or a lost payment.
The defining decision in an upload API is what your servers should *not* do: proxy the bytes. A 2 GB file streamed through your API ties up a connection and a worker for minutes, doubles your bandwidth bill, and turns every deploy into a dropped upload. The design that avoids it splits the flow into three contracts: **create an upload** (your API, small request), **send the bytes** (signed URL, direct to storage — [[file-upload-apis]]), **complete and process** (your API again, returning `202` because scanning takes time — [[long-running-operations]]). Everything else in the study — resumability, checksums, expiry — falls out of taking each of those three steps seriously.
Messaging forces a decision most APIs get to dodge: **which operations belong on request/response and which need a persistent connection?** The answer here is deliberately unglamorous — everything is REST except the one thing that can't be: learning that something happened *now*. Sends, history, read state are plain HTTP because they need retries, caching, and debuggability; a single WebSocket carries only wake-up events ([[websocket-contracts]]). The other defining problem is history pagination: messages arrive constantly, users scroll backwards, and offset pagination produces duplicated or skipped messages within seconds — the textbook case for cursors ([[cursor-pagination]]). Watch how often the design answer is "make the operation idempotent and let the client retry" rather than "make the network reliable".
A notifications API looks like "POST a message" and is actually a small orchestration platform: fan-out across channels, user preferences that can veto everything, scheduling, and delivery that fails hours later inside a third party you don't control. The contract's central honesty is the split between **accepting** a notification and **delivering** it — the API can promise the first synchronously and only *report on* the second, which is why creation returns `202` and delivery lives in its own resource ([[async-job-pattern]]). The second theme: a preference veto is not an error. A user who opted out of marketing email is the system working; the contract must represent that as a delivery outcome, not a failed request.
Search looks like `GET /search?q=` and hides two contract problems that plain list endpoints never face. First, **cost is caller-controlled**: a filter, a fuzzy term, and a deep page multiply into queries that are 1000× more expensive than the median, so the contract needs complexity limits the way an upload API needs size limits ([[large-requests]]). Second, **the result set is a moving target**: documents are created, edited, and re-ranked while the user pages through, and the contract must say what pagination means over data that won't hold still ([[search-apis]]). The design below answers both the same way: promise less, explicitly — bounded depth, snapshot-consistent pages, best-effort counts — rather than implying guarantees the index cannot keep.
An analytics API sells arbitrary computation over large data and must survive doing so. The naive contract — "send any query, get all rows" — dies twice: once when a caller groups by `user_id` over a year and the response is 40 million rows, and again when twelve dashboards refresh at 9am and each fires warehouse scans. The contract's job is to make cost **visible, bounded, and shaped** before execution: a structured query object instead of free-form SQL, admission control with an explainable cost estimate, cursors on every result, and a hard split between **interactive queries** (small, synchronous, cacheable) and **heavy queries** (async jobs with retained results — [[async-job-pattern]]). The recurring move: never let "how much work is this?" be discovered during the work.
An agent run violates every default assumption of request/response: it lasts **seconds to hours**, produces value **continuously** (tokens, tool calls) rather than at the end, **costs real money per second**, and can fail in a dozen partial ways — tool errors the agent recovers from, budget exhaustion mid-thought, a model provider dying between steps. The contract that survives this composes patterns you've seen separately: the async job pattern for lifecycle ([[async-job-pattern]]), SSE with resumable event ids for streaming ([[sse]]), mandatory idempotency because a duplicate run costs dollars ([[idempotency-keys]]), and cancellation as a state transition with honest cost semantics. The novel part is the **event log as the spine**: the run's truth is an append-only, replayable sequence of events, and *everything else* — streaming, polling, billing, audit, resume — is a view over it.