Every tutorial teaches the same REST API: five routes, a happy path, JSON in, JSON out, done in an afternoon. Then the API meets reality — a mobile client that can’t update for three weeks, a partner integration written against a typo you now can’t fix, a security review, a pagination bug that double-charged nobody but double-showed everybody — and you discover that the tutorial covered the easy 20%. The other 80% is what this post is about.
These are the decisions I’ve had to defend (or regret) across years of production APIs, in the order they usually bite. They’re also, not coincidentally, the exact topics the system design interview probes when the interviewer says “now design the API for it.”
How do you version an API you can’t break?
Version from day one, in the URL, and treat every published field as a contract you’ll honor for years. The moment one consumer you don’t control ships against your API, “we’ll clean it up later” becomes fiction: renaming a field is a breaking change, removing one is a breaking change, and changing a type is an outage with your name on it. /v1/ in the path is unglamorous and slightly inelegant — and it’s the approach whose failure modes are boring, which is the highest compliment in API design.
Before going decision by decision, here is the whole post in one table — the tutorial answer, the answer that survives five years of consumers, and what the shortcut actually costs when it comes due:
| Decision | The tutorial answer | The answer that survives | What the shortcut costs |
|---|---|---|---|
| Versioning | Add and rename fields freely | /v1/ from day one; additive changes only after publish | Every rename is an outage inside somebody else’s application |
| Auth default | Add auth to the endpoints that need it | Deny by default; public routes are explicitly marked | The one endpoint someone forgot, found by a crawler before you |
| Authorization | A valid token means the caller is allowed | Verify ownership on every resource fetch | Your customer’s data served to a different customer |
| Errors | HTTP status code plus a prose message | Machine code, human message, correlation ID | Integrators guess; their bug report becomes your 3 a.m. page |
| Pagination | ?page=3 | An opaque cursor meaning “continue after this row” | Items duplicate and vanish under writes; deep pages scan and discard |
| Documentation audience | Humans who will skim it | The most literal consumer you will ever have | An agent hallucinates the parameter you left vague |
The real discipline isn’t the URL scheme; it’s the additive-change habit. New capability? New optional field, new endpoint — never a changed meaning for an existing one. Teams that internalize “published means permanent” design fields more carefully before publishing, which is the actual benefit: versioning discipline is mostly a forcing function for thinking twice.
What does “auth that fails closed” look like?
Every endpoint requires authentication unless explicitly, deliberately marked public — the deny-by-default middleware pattern, where forgetting to configure a route means it’s closed, not open. The industry’s recurring API disaster isn’t broken crypto; it’s the endpoint someone forgot to protect, discovered by a crawler. Fail-closed design makes that mistake structurally impossible: the lazy path and the safe path are the same path.
Authorization deserves the same paranoia one level deeper: authentication says who you are, authorization says what’s yours, and the gap between them is the classic breach. GET /orders/12345 with a valid token must still verify that order 12345 belongs to this token’s user — the missing-ownership-check bug (IDOR, in security parlance) remains the most common serious API vulnerability in code review, and generated code reproduces it enthusiastically because the happy-path version looks identical.
Why do error responses deserve design time?
Because your errors are an API too — the one consumed at 3 a.m. by a stressed integrator deciding whether the bug is theirs or yours. A survivable error response carries three things: a machine-readable code (insufficient_funds, not just HTTP 400), a human-readable message that names the offending field, and a correlation ID that lets your logs and their bug report find each other. HTTP status alone is too coarse — a 400 that means five different things forces every consumer to parse your prose.
Predictability is a feature. Surprise is a cost. The APIs people call “pleasant to integrate” are rarely clever — they do the same thing everywhere.
Consistency beats richness. One error envelope, identical shape on every endpoint, documented once. The APIs people describe as “pleasant to integrate” are rarely doing anything clever — they’re doing the same predictable thing everywhere, so the integrator’s error handling is one function instead of one per endpoint. Predictability is a feature; surprise is a cost, the same lesson durability teaches at the architecture level.
What’s wrong with page-number pagination?
Offset pagination (?page=3) lies under concurrent writes: rows inserted or deleted while a consumer walks the pages cause items to appear twice or vanish, and deep offsets punish the database with a full scan-and-discard. It survives in tutorials because it’s trivial and looks right in demos with static data. Cursor pagination — an opaque token meaning “continue after this row” — stays correct under writes and stays fast at depth, at the cost of losing “jump to page 7,” which most real consumers never needed.
The transferable rule: paginate by position in an ordering, not by count from the start, whenever data changes underneath readers. It’s a small design decision with a long tail — retrofitting cursors onto a shipped offset API is exactly the kind of published-contract migration the versioning section warned about.
Does any of this change when the consumer is an AI agent?
The principles survive; the tolerances tighten. Agents consume APIs through tool definitions, and everything above becomes machine-facing: a vague error message a human would shrug past sends an agent into a retry loop, an inconsistent envelope breaks its parsing, an under-documented parameter gets hallucinated. Designing APIs that agents can use reliably — precise schemas, exhaustive error codes, honest descriptions — is the same craft as good REST design with the sloppiness allowance removed. That’s the thesis of the MCP explainer: tool interfaces are API design, round two, with a less forgiving consumer.
Which is why the fundamentals course path still runs through building real REST services — Node.js and production APIs for the craft itself, on the backend architecture floor that decides what’s a service and what’s a handler, with TypeScript as the typing discipline that makes contracts enforceable instead of aspirational. Every one of those skills now has a second customer: the agent calling your API through a schema, exactly as literal-minded as the type checker, and considerably more expensive when confused.
The survival checklist
Version in the URL from day one; additive changes only after publish. Deny-by-default auth; ownership checks on every resource fetch. One error envelope everywhere: machine code, human message, correlation ID. Cursors, not offsets, wherever data moves. Document for the most literal consumer you’ll ever have — because as of this year, you have one. None of it is glamorous. Neither is a bridge that doesn’t fall down.