Start With Requirements, Not Endpoints
The first API artifact should be a list of questions answered, not a list of routes. Who consumes it, what can be retried, what is destructive, and what must stay compatible — endpoints fall out of those answers.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
The questions that come before any route
Given "users can create projects, invite members, assign roles, and remove members", most engineers start typing POST /projects. The endpoint is not wrong — it is premature. Every answer below changes the contract, and none of them can be bolted on later without breaking someone.
The questions are cheap now and expensive later. "Can requests be retried?" costs one sentence in a design doc; discovering the answer in production costs a duplicate-charge incident and an Idempotency Keys: The Mechanism retrofit across every client.
- Who are the consumers? Browser, mobile app, CLI, service-to-service, third-party developers — each pulls the design differently (see Consumer-First Design).
- Public or internal? Decides the compatibility horizon: quarters versus years (see Public vs Internal APIs).
- Are operations synchronous? A report that takes minutes must not pretend to be a request/response call (see Long-Running Operations: 202 and the Job Resource).
- Can requests be retried? If yes — and the network guarantees yes — which operations need idempotency protection?
- Are actions destructive? Deletion and money movement need confirmation semantics, soft-delete windows or explicit state machines.
- Is offline usage relevant? Mobile clients that sync later need conflict answers, not just endpoints.
- How important is compatibility? An API for one internal team can iterate; one shipped in a partner SDK cannot.
From requirement to contract, in order
The loop this domain teaches runs requirement → consumers → resource model → style → contract → validation → authorization → errors → idempotency → pagination → versioning → observability → evolution. The order is the point: each step constrains the next, and skipping to "style" or "endpoints" means deciding the constrained steps by accident.
Notice what the order implies about the famous debates. "REST or GraphQL?" is step four, not step one — it cannot be answered before consumers and the capability model are known. "What should we name the endpoint?" is even later. The debates teams have first are the ones that matter least.
A worked example: the same requirement, two different APIs
The requirement "users can remove members" produces different contracts depending on the answers. If removal is rare, synchronous and reversible only by re-inviting, DELETE /projects/{id}/members/{userId} returning 204 is honest. If removal must strip access immediately but archive the member's contributions asynchronously, the honest contract is a state change the caller can observe.
Neither is "more RESTful". One matches a requirement where deletion is an event; the other matches a requirement where deletion is a *process*. The requirement decided — the style debate never had to happen.
1DELETE /projects/42/members/72→ 204 No Content3 4# Later discovered in production:5# - removal also has to archive contributions (takes ~40s)6# - the 204 lied; the member still had access for a while7# - clients that retried the slow call got 404s1POST /projects/42/members/7/removals2→ 202 Accepted3{ "id": "rem_1", "status": "revoking_access" }4 5GET /projects/42/members/7/removals/rem_16→ { "status": "archiving", "access_revoked": true }The second contract is not fancier — it is truthful about a requirement the first one ignored. Asking "is this synchronous? is it retried?" before choosing the endpoint shape is what surfaced it.
Key points
- Endpoints are outputs of design, not inputs: consumers, retryability, destructiveness and compatibility horizon come first.
- Every unanswered requirement question becomes an accidental contract decision made by the implementation.
- The design loop is ordered — consumers before model, model before style — because each step constrains the next.
- Style debates (REST vs GraphQL vs RPC) are unanswerable and unnecessary before the consumer and capability questions are settled.
- One requirement can honestly produce different contracts; the requirement answers decide, not taste.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → whiteboard: starts from "we need CRUD for projects" and writes routes before requirements.
- 2Routes → implementation: synchronous request/response is assumed because it is the default, not because it fits.
- 3Reality → API: a retry, a slow operation and a destructive action arrive; the contract has no answer for any of them.
- 4Team → patches: idempotency, async status and soft-delete are bolted on per-endpoint, each with different semantics.
- 5Consumers → integration: every endpoint now behaves slightly differently, and the API's real contract is "read the source".
- Inconsistent retro-fitted semantics: three endpoints, three different answers to "what happens on retry".
- Redesign under load: the contract must change exactly when the most consumers depend on it.
- Consumer trust: integrators stop believing the documentation and code against observed behavior instead.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Run the requirement questions (consumers, sync/async, retries, destructiveness, compatibility) as a written checklist before any endpoint review.
- • Reject endpoint proposals that cannot cite the requirement answers they derive from.
- • Prototype the consumer's code first — write the calling code you wish existed, then design the API that makes it true.
- • Keep the requirement answers in the design doc as the contract's "why"; future maintainers change contracts safely only when they know which requirement each clause serves.
- • Design reviews that argue about naming while retry semantics are undecided signal endpoint-first thinking.
- • Endpoints acquiring `?force=true`, `?async=true` or `?dry_run=true` parameters after launch are requirement questions surfacing late.
- • A growing pile of per-endpoint special cases in client SDKs shows the contract was assembled, not designed.
- • Requirement answers age well: when a new consumer type arrives (say, a CLI), re-running the checklist shows exactly which contract clauses need extension.
- • Documented requirement→clause traceability makes deprecation honest: you can tell whether the requirement died or just the shape.
- • Answering the checklist honestly takes days on a contract that could be "shipped" in hours — the payback is invisible until the first retry or slow operation.
- • Some answers will be wrong anyway; the value is that wrong *explicit* answers are findable and fixable, unlike wrong assumptions.