Learn API Design

Start from the consumers and the requirement, model the resources and their states, choose a style with named trade-offs, then design the contract clauses — errors, idempotency, pagination, versioning — that keep it predictable and evolvable.

API Fundamentals

An API is a behavioral contract, not a list of endpoints. Requirements before endpoints, consumer tasks before resources, granularity, ownership — and why internal APIs still need contracts.

What an API Contract Actually Is
▶ lab

An API is a behavioral contract, not a list of URLs. The contract includes shapes, errors, retry behavior, ordering, consistency and rate limits — everything a client is forced to assume, whether you documented it or not.

Q · When a client calls this API, what exactly has it been promised?
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.

Q · What must be true about callers, retries, destructiveness and compatibility before any endpoint is worth writing down?
Consumer-First Design

APIs exist for consumer tasks, not for the provider's data model. A mobile dashboard, a partner's invoice integration and an internal inventory call want different granularity, different fields and different guarantees from the same domain.

Q · Which consumer tasks must this API make easy, and what does each consumer's environment demand from the contract?
API Granularity and the Chatty API

Too fine and every task takes ten round trips; too coarse and every call hauls a kitchen sink. Granularity is a per-consumer decision, and the network — not aesthetics — is what punishes getting it wrong.

Q · Is the boundary of each operation matched to the tasks and network position of the consumers that call it?
Public vs Internal APIs

The difference is not importance — it is who absorbs the cost of change. Public APIs trade evolution speed for a long compatibility promise; internal APIs may iterate faster only while every consumer is known and reachable.

Q · Who consumes this API, can you make them upgrade, and what does that answer let you promise?
API Ownership and the Catalog

Every API needs an owner, a version, a consumer list, an SLO and a deprecation status that a stranger can find in one place. An API nobody owns is a contract nobody keeps.

Q · For every API in the org: who owns it, who consumes it, what does it promise, and what is its lifecycle status?
Design Principles Without Commandments

Consistency, predictability, explicitness, least surprise, good defaults, bounded operations. Principles earn their place as tie-breakers and review questions — not as absolutes that override a requirement.

Q · When two designs both satisfy the requirement, which one will consumers predict correctly without reading the docs?
API Anti-Patterns Field Guide

Everything-POST, verb explosion, raw DB models as contracts, unbounded lists, 200-for-everything, frontend-only authorization, versioning nothing or versioning everything. Recognizing the pattern is faster than rediscovering the pain.

Q · Which of the known failure shapes is this design about to reproduce?
Resource & Capability Modeling

From domain to resources, from resources to operations. Resource vs action, state machines with explicit transitions, backend-for-frontend, and composition — without REST dogma.

From Domain to Resources

Resources are the nouns your consumers need to point at — not your tables, not your classes. Deriving /users, /projects, /memberships and /invitations from one requirement shows the reasoning; the paths are just the residue.

Q · Which concepts in this domain deserve a stable, addressable identity in the contract — and which are just attributes of something else?
Resource or Action?

POST /cancelOrder, POST /orders/{id}/cancellations, PATCH {status: "cancelled"} — three shapes for one operation, each promising something different. Actions with their own data and lifecycle are domain concepts worth modeling; the rest can stay verbs or field updates.

Q · Is this operation a state edit, a command, or a domain concept with its own data — and which shape tells consumers the truth about it?
Resources Have State Machines

An order moves created → paid → processing → shipped → delivered, and not one step in any other order. If the contract does not say which transitions exist, every consumer invents its own machine — and the server enforces a third one.

Q · Which states can this resource be in, which transitions are legal, who may trigger each — and does the contract say so, or do consumers guess?
Designing State Transitions

PATCH {status: "shipped"} makes the client the owner of the machine; POST /orders/{id}/ship makes the server own it. Command-style transitions carry data, enforce guards, and answer retries — at the cost of one endpoint per transition.

Q · Should clients edit the state field directly, or request transitions through operations the server owns?
The "Everything Is CRUD" Trap

CRUD describes storage, not behavior. Payments, approvals, workflows and agent runs have states, guards and side effects that create/read/update/delete cannot say — flattening them into updates hides exactly the semantics consumers must know.

Q · Does create/read/update/delete actually describe what this domain does — or is UPDATE about to become a trapdoor for every behavior the contract refuses to name?
Backend for Frontend

A BFF is an API whose consumer is one client experience: the web app or the mobile app, not "clients in general". It buys screen-shaped responses and per-client iteration speed, and costs an extra service per client type — a price not every team should pay.

Q · Should each client experience get its own API layer, or should all clients share one general-purpose surface?
Composed APIs: Aggregating Other Services

An endpoint that answers by calling four other services inherits four latencies, four failure modes and four teams' release schedules. Composition buys consumers one call instead of N — and the contract must say what happens when one of the N fails.

Q · When one endpoint aggregates several internal services, what does it promise about latency, freshness and behavior when a dependency is down?
HTTP Semantics

Methods as promises: safety, idempotency, and what retries, proxies and caches are allowed to assume. Status codes that mean something, conditional requests, and caching as part of the contract.

HTTP Methods Are Promises
▶ lab

Safe means "calling this changes nothing"; idempotent means "calling this twice equals calling it once". Retrying clients, proxies, caches and crawlers all act on those promises without asking — which is why breaking them breaks things you have never heard of.

Q · What is each HTTP method allowed to promise about side effects and repetition — and which infrastructure is already acting on that promise?
GET: The Promise of Safety

GET promises that reading changes nothing — a promise browsers, caches, crawlers and prefetchers spend billions of requests a day relying on. GET /deleteUser?id=42 is not a style violation; it is an open invitation to every robot on the internet.

Q · Can every GET in this API be issued by anything, any number of times, at any moment, without changing state anyone is accountable for?
POST: More Than Create

POST is HTTP's "here, process this" — creation, commands, complex reads, batch submissions. Its defining property is what it refuses to promise: idempotency. Every POST that matters needs an answer to "what if this arrives twice?", because it will.

Q · This operation is not safe and not idempotent by method — so what is the application-level answer when the same POST arrives twice?
PUT vs PATCH

PUT replaces the whole representation and is idempotent by construction; PATCH applies a partial change and is only as safe as your merge rules. The hard part is not choosing between them — it is saying what null means, and what absent means.

Q · When a client updates this resource, is it stating the complete desired state (PUT) or requesting a delta (PATCH) — and does the contract define what null and absent each mean?
DELETE: What Does Gone Mean?

Hard delete, soft delete, async purge — three different promises hiding behind one method. DELETE is idempotent (the retry that gets 404 still succeeded), but what deletion *means* — recoverable? invisible? eventually erased? — is a domain contract HTTP cannot write for you.

Q · When a client DELETEs this resource, what is actually promised — immediate erasure, hidden-but-recoverable, or a purge process — and what does a retry see?
Status Codes Clients Can Branch On
▶ lab

The first digit answers "who acts next?" — that is the real contract. You need the dozen codes clients actually branch on, used honestly, far more than you need the other forty memorized.

Q · For every response this API sends, does the status code correctly tell the client — and every cache, proxy and monitor in between — who should act next, and how?
Conditional Requests: ETags, 304 and 412

One mechanism, two superpowers: If-None-Match turns repeat reads into 200-byte 304s, and If-Match turns racing writes into honest 412s. The validator — the ETag — is a contract about when a representation counts as changed.

Q · Can clients ask "has this changed?" instead of re-downloading, and say "apply this only if unchanged" instead of overwriting — and what exactly does the ETag promise?
Caching as a Contract Clause

Cache-Control is not a performance knob — it is a promise about staleness: who may store this response, for how long, and what "fresh enough" means. The most expensive header in HTTP is the one that let a shared cache store a private response.

Q · For each response: who is allowed to cache it, for how long may they serve it without asking, and what staleness has the consumer actually agreed to?
API Styles

REST, RPC, gRPC and GraphQL as tools with prices, not religions. What each buys, what each costs operationally, and how consumer environment decides — never “which one is best”.

Which API Style Should I Use?

REST, RPC/gRPC, GraphQL, SSE, WebSockets, webhooks, async jobs — seven shapes, each answering a different question about who the consumer is and how data needs to move. The decision is made by consumer environment and operational budget, not by fashion.

Q · Given who consumes this API, where they run, and how data needs to flow, which contract shape fits — and what does that shape commit the team to operating?
REST as a Practical Style

REST is a set of constraints — addressable resources, uniform methods, representations, statelessness, cacheability — that let the whole HTTP ecosystem work for you unmodified. It is not "CRUD over HTTP", and its value is in the guarantees, not the URL aesthetics.

Q · What does designing "RESTfully" actually buy a consumer — and which of the constraints are worth keeping when the domain pushes back?
The "REST Purity" Anti-Pattern

Contorting every operation into one interpretation of REST hides domain semantics behind status flips and produces contracts nobody can read. Clarity and domain meaning outrank purity — and so does the opposite ditch, where "REST is limiting" excuses a verb for everything.

Q · When the domain pushes back against the resource model, do you bend the domain or bend the style — and how do you tell a principled exception from a lazy one?
RPC: Operation-Oriented Contracts

RPC contracts are lists of operations — `UserService.GetUser`, `InventoryService.ReserveInventory` — rather than resources with methods. When the domain is a set of commands between services, that is clearer than bending them into nouns; it costs caching, discoverability and verb discipline.

Q · When is "call this operation with these arguments" a clearer contract than "act on this resource with this method" — and what does the operation-shaped style give up?
gRPC: Schema, Codegen and Streams

gRPC is RPC with a schema language (protobuf), generated clients, binary framing and four call shapes including streaming, on HTTP/2. It buys enforced contracts and efficient internal traffic; it costs browser friendliness, readability, and a proto discipline that decides whether evolution is safe.

Q · What does a schema-enforced, binary, streaming RPC contract buy internal consumers — and what does it commit the team to when browsers, partners and evolution show up?
GraphQL: Client-Shaped Queries Over One Schema

GraphQL replaces many endpoints with one typed schema that clients query for exactly the fields they need: queries, mutations, subscriptions, resolvers behind each field. The benefits — no over/under-fetching, one contract for many client shapes, introspection — are real; so are the costs, which get their own lesson.

Q · When many clients need different slices of one connected data graph, what does letting the client choose the shape buy — and what does the server take on to make that safe?
What GraphQL Costs
▶ lab

The flexibility GraphQL gives clients is exposure the server must manage: resolver N+1, arbitrary expensive queries, per-field authorization, lost HTTP caching, invisible operations. Batching, cost limits, persisted queries and operation-level telemetry are the price — budget it before adopting the schema.

Q · Who bounds the cost of a query you never anticipated — the schema, the resolvers, or the outage — and what machinery moves that answer from "outage" to "schema"?
Request & Response Design

Required vs optional vs null, response models that are not database rows, over- and under-fetching, batch endpoints, size limits, and file uploads that bypass the API for the bytes.

Request Contracts: Required, Optional, Null and Absent

A request schema is a set of promises about what the server will accept and what each field means. Required vs optional vs nullable, "not sent" vs "sent as null", enums, defaults and the unknown-field policy decide whether the contract can grow — or whether every addition breaks someone.

Q · For every field a client can send, what does the contract say about absence, null, defaults and values it has never seen — and does the server behave the same way in every operation?
Response Contracts Are Not Database Rows

Serializing the ORM model is the fastest way to ship an endpoint and the most expensive way to own one. A response model is a purpose-built shape — stable ids, explicit types, computed fields, nothing accidental — that lets the table change without the contract noticing.

Q · What does this response promise consumers — and how much of that promise is really just the current shape of a table that nobody intended to freeze?
Over-Fetching and Under-Fetching

GET /users/42 returns 80 fields when the screen needs 3; the dashboard makes 8 calls to render once. Both are granularity mismatches between one generic contract and many specific consumers — and the fixes (sparse fieldsets, expansion, GraphQL, a BFF) each move the cost somewhere else.

Q · Whose screen is this contract shaped for — and when it is shaped for nobody in particular, who pays: the network, the server, or the client that makes eight round trips?
Batch APIs and Partial Failure

A client that needs 500 resources can make 500 requests or one. The batch endpoint saves round trips and rate-limit budget — and forces the contract to answer questions a single request never asked: what if item 217 fails, is anything rolled back, and how many requests did that just cost?

Q · When one request carries many operations, what does the contract promise about each of them — success, failure, order, atomicity — and what does the caller do with a response that is half of each?
Large Requests and Documented Limits

Every API has limits on body size, array length, string length, query complexity and file size. The only question is whether the contract states them — with a status code and the number — or whether a load balancer, a JSON parser or the OOM killer states them for you.

Q · How big can a request be before this API refuses it — in bytes, items, nesting depth and query cost — and does the client learn the limit from the docs or from a stack trace?
File Upload APIs: Authorize, Upload Directly, Confirm

The API that handles JSON should not be the pipe for a 3GB video. Create an upload resource, hand the client a signed URL to object storage, confirm completion, then process asynchronously — a four-step contract that keeps the API small, the bytes off your workers, and retries safe.

Q · Where do the bytes go, who authorizes them going there, how does the API learn the upload finished — and what does the client see while a 3GB file is being processed?
One Vocabulary: Naming and Consistency

A consumer who has learned one endpoint should be able to predict every other. Casing, id formats, timestamps, money, envelopes, error shapes and header names are decided once, written in a style guide and enforced by a linter — because consistency is the cheapest documentation an API will ever have.

Q · If a consumer learns one operation of this API, how much of the rest can they guess correctly — and what enforces that the guess keeps being right as ten teams add endpoints?
Error Models

A stable error contract: machine-readable codes, a taxonomy clients can branch on, field-level validation feedback, explicit retryability, and honest partial-failure semantics.

The Error Model: Structure Over Apology

A failing response is still a response, and clients write code against it. A stable error model — machine-readable code, human message, request id, structured details — is a contract clause, not a courtesy.

Q · When this API fails, what exactly does the client learn, and what can its code do about it?
An Error Taxonomy Clients Can Branch On

Validation, authentication, authorization, not-found, conflict, rate-limit, dependency, internal: eight categories with different owners, different fixes and different retry rules. Collapse them and every client guesses; distinguish them and clients can be correct.

Q · Which distinct kinds of failure can this API produce, and does the contract let a client tell them apart mechanically?
Validation Errors: Feedback, Not Verdicts

Reject invalid input at the boundary, and say exactly which field failed which rule — all of them, in one pass. A validation error is a collaboration with the caller; "bad request" is a verdict nobody can act on.

Q · When a request fails validation, does the response let the caller fix every problem in one round trip — mechanically?
Retryability: Telling Clients What To Do Next

Every error answers a question the client is definitely asking: do I try again? A contract that states retryability explicitly — status semantics, Retry-After, a retryable flag — replaces a thousand guessed retry loops with one correct one.

Q · For every failure this API can produce, does the contract say whether, when and how the client should retry?
Partial Failure: When 3 of 5 Succeed

A batch request where some items succeed and some fail has no honest single status code. The contract must choose — atomic, best-effort with a per-item report, or a mix — and say so before the first consumer assumes the wrong one.

Q · When one request carries many operations and only some succeed, what does the response claim — and what is the caller supposed to do next?
Pagination, Filtering & Search

Every list endpoint is a query API. Offset vs cursor under concurrent writes, stable ordering, filter allowlists, search as a different contract — and the index each promise requires.

Pagination: Choosing How Lists End
▶ lab

Every list endpoint needs an answer to "and then what?" before the collection grows. Offset, cursor and keyset pagination are different promises about consistency, cost and navigation — and the consumer's access pattern picks, not fashion.

Q · How does a client traverse this collection, and what happens to its traversal when the collection changes underneath it?
Offset Pagination: Simple, Jumpable, and Lying Under Writes
▶ lab

`?page=3&limit=50` is the easiest pagination to build and consume, and it makes two quiet promises it cannot keep at scale: that deep pages are as cheap as shallow ones, and that page boundaries hold still while the collection changes.

Q · What does "page 3" actually promise, and what does serving it cost when the collection is large and moving?
Cursor Pagination: An Opaque Bookmark, Not a Position
▶ lab

A cursor is the server saying "resume after this row" in a token the client stores but never reads. Done right it makes deep traversal flat-cost and write-stable; done lazily it leaks internals, breaks on deploys, and quietly becomes offset with extra steps.

Q · What exactly does the continuation token encode, what does it promise across time and deploys, and what happens when it is stale?
Filtering: An Allowlist With an Index Bill

Every filter parameter is a promise that a class of database queries will stay fast forever. Explicit, typed, allowlisted filters keep that promise affordable; a generic query language hands your query planner to strangers.

Q · Which subsets of this collection does the contract promise to serve efficiently — and which combinations did you just promise by accident?
Sorting: Determinism or Drift

An ORDER BY in the contract is two promises: that the ordering is affordable, and that it is deterministic. Skip the tiebreaker and pagination corrupts; allowlist nothing and every column is an index you owe.

Q · Is every ordering this API offers total, deterministic, index-backed — and did the docs say which one applies when the client says nothing?
Search Is a Different Contract Than Filtering

Filtering promises the exact subset matching a predicate; search promises the most *relevant* results for an expression of intent. Different guarantees, different cost model, different pagination — pretending one is the other breaks both.

Q · Is this endpoint promising exact membership in a predicate, or ranked relevance to an intent — and does its contract (results, pagination, consistency) match the promise?
Unbounded Collections: The Anti-Pattern With a Fuse

GET /orders returning "all of them" works flawlessly until the collection grows — then it fails everywhere at once, and the fix is a breaking change to every consumer. The bound you did not design is the outage you scheduled.

Q · What is the largest response this endpoint can produce — and did you choose that number, or is it whatever the table holds that day?
Idempotency & Concurrency

The network loses responses, so clients retry. Idempotency keys, dedup vs idempotency, optimistic concurrency with versions, lost-update prevention, and consistency the contract admits to.

Idempotency: Surviving the Retry
▶ lab

A response can be lost after the server did the work, so every client will eventually retry a request that already succeeded. Idempotency is the contract property that makes that retry safe — and money paths without it double-charge.

Q · When the same request arrives twice — and it will — does the system produce one effect or two?
Idempotency Keys: The Mechanism
▶ lab

A client-generated key turns "did my POST land?" into a question the server can answer: check the store, replay the saved result or process and save. The hard parts are scope, expiry, parameter mismatches, and two identical requests in flight at once.

Q · How does the server recognize a retry of a request it already processed — and what exactly does it return for one?
Idempotency vs Deduplication

Idempotency makes a repeated request produce the same outcome and hands that outcome back. Deduplication detects that a message was already seen and drops it. Related, frequently confused — and each one fails when asked to do the other's job.

Q · Do I need to answer a repeated request with its original outcome, or silently discard a repeated message?
Optimistic Concurrency: Versions and If-Match
▶ lab

Let concurrent writers proceed without locks, but make every update state which version it read. A stale version gets a 409 or 412 instead of silently destroying someone else's write — and the contract must say who untangles the conflict.

Q · When two clients update the same resource from the same starting point, how does the second one find out — and what is it supposed to do then?
The Lost Update, Step by Step
▶ lab

A reads v1, B reads v1, A writes, B writes — and A's change is gone without an error, a log line, or a conflict. The anatomy of the most silent data-loss bug an API can have, and what a version check turns it into.

Q · Between a client's read and its write, someone else wrote — whose change survives, and does anyone find out?
Consistency as a Contract Clause

A client POSTs a resource, then GETs it — and gets a 404. Nothing is broken unless the contract said otherwise. Read-after-write, monotonic reads and staleness bounds are promises consumers build UI and logic on, so they must be written down.

Q · After a successful write, which reads see it, when — and did we tell consumers, or leave them to find out?
There Is No Transaction Across APIs

One request that charges payment, reserves inventory and books shipping cannot be atomic — the ACID boundary died at the first network hop. What replaces it is a contract that models the in-between states: workflows, state machines and compensation.

Q · This operation touches three systems and the second one just failed — what state is the caller looking at, and what does the contract say about it?
Retries and Timeouts as Contract Guidance

A timeout is not a failure — it is the absence of an answer. The contract owes clients the missing half of their retry loop: what is retryable, how long to wait, how to back off, and what the server will do to protect itself when everyone retries at once.

Q · The request failed or timed out — should this client try again, when, how many times, and does the server survive everyone deciding "yes"?
Real-Time & Async Operations

When request/response stops fitting: WebSocket message contracts, SSE, streaming, the async job pattern for long-running work, and how completion actually reaches the client.

WebSocket Message Contracts

A WebSocket gives you a pipe, not a protocol. Everything HTTP provided for free — operations, status codes, request/response pairing — you must now design: typed message envelopes, acks, errors, sequence numbers, and a reconnect story clients can actually implement.

Q · Once the connection upgrades, what may each side send, what must each side answer, and how does a client that vanished for eight seconds get back to a correct state?
Server-Sent Events

One long-lived HTTP response, streaming events one way: server to client. SSE buys auto-reconnect with built-in resume (Last-Event-ID) for the price of unidirectionality — and for notifications, progress, dashboards and token streams, one way is all you needed.

Q · The server has a stream of events for the client — does the client need to talk back on the same channel, and if not, why carry a bidirectional protocol's costs?
Streaming APIs: Partial Data as a Contract

A streamed response is a sequence of commitments, not one answer. The contract must say what each chunk means, whether early chunks can be trusted before the end, how the stream announces failure mid-flight, and what a consumer resumes after a drop.

Q · When the response arrives in pieces over time, what may the consumer do with the pieces it has — and how does it learn the stream ended well, ended badly, or never really ended at all?
Long-Running Operations: 202 and the Job Resource
▶ lab

A request that takes 15 minutes cannot pretend to be request/response — some timeout between the client and your handler will fire first, and a retry starts the 15 minutes again. Return 202 with a job resource instead, and the operation becomes observable, retry-safe and cancellable.

Q · This operation takes longer than any hop in the chain will keep a connection open — so what does the client get back now, and how does it reach the result later?
The Async Job Pattern

POST the operation, get 202 and a job resource, let a worker do the work, poll or be notified, fetch the result. The pattern is simple; the contract is not — queued/running/succeeded/failed/cancelled is a state machine with retention, cancellation, progress and idempotent creation that consumers build whole workflows on.

Q · When the work outlives the request, what resource does the client hold, which states can it be in, and how does the client get the result, cancel, or retry safely?
How the Client Learns the Job Finished

Polling, webhooks, SSE/WebSocket, push notification — four ways to say "done", each with a different latency, infrastructure cost, client requirement and duplicate story. Polling with Retry-After is the documented baseline every client can use; the others are upgrades for specific consumers.

Q · For each kind of consumer, which channel delivers "your job is done" reliably enough — and what does the contract promise when that channel duplicates, reorders or misses a notification?
Slow Clients and Backpressure

A streaming or download API produces bytes faster than some consumer can take them. Where do the bytes wait, who runs out of memory first, and when does the server hang up? A contract that does not answer those questions answers them in production — usually by the whole tier falling over together.

Q · When a consumer reads more slowly than the API produces, what bounds the buffered data, what the contract promises about ordering and loss, and when the server is allowed to disconnect?
Webhooks

Your contract running against someone else’s server: delivery states, retries, duplicate events, ordering you must not assume, and the signature that makes any of it trustworthy.

Webhooks: The Inverted Contract

A webhook flips the roles: the provider becomes the client, calling an endpoint the consumer operates. Delivery is asynchronous and at-least-once, so the event envelope — event id, delivery id, type, timestamp — is what makes the stream usable, not the payload.

Q · When the provider calls the consumer instead of the other way around, what must the event contract state for the consumer to build something reliable on it?
Webhook Delivery: States, Retries, Redrive
▶ lab

Every event delivery is a little state machine: queued → attempting → delivered, or failed → retrying → dead. The retry schedule, the definition of "delivered", and the dead-letter escape hatch are contract clauses both sides build against.

Q · What exactly does the provider promise about when, how often, and for how long it will try to deliver each event — and what happens when it gives up?
Consumer-Side Idempotency

The provider promised at-least-once, so duplicates are not a bug — they are scheduled. Exactly-once processing is an illusion the consumer manufactures locally: record the event_id, process each id exactly once, and make the recording atomic with the effects.

Q · When the same event arrives twice — and it will — how does the consumer make the second arrival a no-op instead of a second shipment?
Webhook Ordering: Assume None

Retries, parallel dispatch and redrives mean events arrive in whatever order the network permits — `order.shipped` before `order.paid` is routine. Consumers that apply event payloads as state, in arrival order, corrupt their data; the contract must say so and give them a defense.

Q · What may a consumer assume about the order in which events arrive — and since the honest answer is "nothing", how do they keep their state correct anyway?
The Webhook Security Contract

A webhook receiver is an unauthenticated public POST endpoint that triggers business logic — unless the contract says how events are signed, how timestamps bound replay, and how secrets rotate. Signature verification is the consumer's only proof that an event is yours.

Q · How does a consumer prove an incoming event actually came from the provider, is unmodified, and is not a replay — and how does the contract make that verifiable forever, across secret rotations?
API Security Boundary

Where authentication and authorization live in the contract: token placement, resource-level permission design, scopes, API keys, rate limits and quotas as documented behavior.

Authentication in the Contract

The contract does not implement authentication — it states which credential each consumer type presents, where it rides, how long it lives, and exactly what a 401 means. The mechanisms are Security Engineering's domain; the promises are yours.

Q · Which credential does each consumer of this API present, where does it go in the request, and what is the documented behavior when it is missing, expired or wrong?
Authorization Design in the Contract

Every operation needs a documented answer to "who may call this?" — and the enforcement must check the *object*, not just the endpoint. Missing object-level checks are the most exploited API flaw in the wild, and the contract decides whether denial reads as 403 or 404.

Q · For every operation and every resource id in it: who is allowed, how does a consumer discover that from the docs, and what exactly happens when the answer is no?
Scopes: Least Privilege as Contract Surface

A scope caps what a credential may ask for — `projects:read` cannot touch billing even if the user behind it can. Too coarse and every integration holds admin; too fine and nobody can predict which scope an endpoint needs. The catalog is the contract.

Q · How does the contract let a consumer request exactly the access its integration needs — and no more — in units both the consumer and the resource owner can understand?
API Keys: Identity for Applications

An API key identifies an application — which makes it the natural unit for scoping, rate limiting and metering, and the wrong tool the moment a user is delegating access. Keys are credentials: prefixed, hashed at rest, scoped, and rotatable without downtime.

Q · What does an API key actually identify, what lifecycle must the contract support for it, and when is a key the wrong credential entirely?
The Rate-Limit Contract

Every API has a rate limit — the only question is whether it is a documented 429 with headers or an undocumented collapse. The contract names the dimensions (per key, per user, per endpoint class), the numbers, and exactly how a well-behaved client should respond.

Q · What does a client that hits the limit actually experience — and does the contract give it enough information to slow down correctly instead of retrying itself into a ban?
Quotas vs Rate Limits

A rate limit protects the platform second by second; a quota is an entitlement over a billing period. 100 requests/second and 1M requests/month are different promises with different rejections, resets and communication duties — conflating them breaks both.

Q · Is the caller being slowed because the platform needs protecting right now, or stopped because they have consumed what their plan entitles them to — and does the contract distinguish the two?
Versioning & Evolution

The longest-lived part of the contract. Additive change, enum evolution, deprecation as a process, consumer telemetry before removal, schema-first vs code-first, docs and SDKs.

Versioning: What a Version Even Promises

URI versions, header versions, date-pinned versions, or no versions at all — the strategies differ less than the arguments suggest. What matters is what a version promises, what minting one costs, and why additive evolution is the strategy every good API uses between versions.

Q · What does a version number actually promise consumers, and which changes are worth the price of minting one?
Backward Compatibility: The Real Rules
▶ lab

The safe list and the breaking list are shorter and stranger than intuition says. Adding an optional field is safe; making an optional field required is not; tightening validation, changing a default, or changing what a value means breaks clients without touching a single field name.

Q · Which changes to this contract can ship today without breaking a single existing consumer — and which only look like they can?
Enum Evolution: The New Value That Broke Old Clients
▶ lab

You add `suspended` to a status enum — additive, surely safe. Every old client that switched exhaustively over the closed set now throws, hides the record, or worse, treats it as `active`. Enums are the sharpest edge of compatibility, and the fix is a contract clause, not a code change.

Q · When the server returns a value this client has never heard of, what does the contract say the client must do?
Removing Fields Without Removing Consumers
▶ lab

Addition is a deploy; removal is a program. Introduce the replacement, measure who still reads the old field, deprecate it visibly, run a real migration window, and remove only when telemetry — not hope — says zero. The steps are boring; skipping any of them is an outage.

Q · What has to be true — and measured — before a field that consumers once read can safely disappear?
Deprecation as a Process, Not a Label

Marking something deprecated changes nothing; deprecation is a campaign with artifacts — announcement, migration guide, machine-readable signals, telemetry, a deadline someone will enforce — and a finish line. A deprecation nobody plans to complete is just an apology in advance.

Q · When this contract element must go away, how does every affected consumer find out, migrate, and confirm — before the deadline does it for them?
Consumer-Driven Evolution: Telemetry Before Breakage

"Can we remove this?" is a telemetry query, not a debate. Per-consumer, per-field usage attribution turns evolution decisions from opinions into evidence — and the 12% of mobile users on an old build stop being invisible exactly when you can count them.

Q · Before this contract element changes, do we know — per consumer, with numbers — who depends on it and how much?
API Migration: Running the Change End to End

Every breaking change, whatever its label, runs the same program: ship the new surface, support both, move consumers with telemetry and deadlines, deprecate, remove. The compatibility matrix — which client works against which API — is the map; the burn-down is the engine.

Q · How does every consumer get from the old contract to the new one without an outage on either side of the change?
Schema-First vs Code-First

Whether the contract file or the handler code comes first matters less than which one is the enforced source of truth. Schema-first buys review-before-build and cross-team parallelism; code-first buys iteration speed; drift — where the served API and the described API diverge — is the failure mode both must engineer away.

Q · Which artifact is the source of truth for this contract, and what mechanically prevents the served API from drifting away from it?
OpenAPI: Describing the Contract, Not Designing It

OpenAPI captures paths, operations, schemas and security schemes in a machine-readable file — which earns you linting, diffing, mocks, generated clients and always-current reference docs. What it cannot capture is most of what this domain teaches: guarantees live in prose, and the spec is the skeleton they hang on.

Q · What can a machine-readable description of this API do for its consumers and its governance — and which parts of the contract will it never contain?
Documentation Is Part of the Contract

For every consumer you never meet, the docs are the API. What must be documented is exactly what consumers are forced to assume — auth, errors, pagination, rate limits, idempotency, guarantees — and the examples are the most-executed code you ship. Undocumented behavior gets reverse-engineered and depended on anyway.

Q · Can a developer who will never talk to your team integrate correctly — including failure handling — from the docs alone?
SDK Design: The Contract's User Interface

`payments.create({...})` versus hand-rolled HTTP is the visible part. The invisible part is what the SDK owns on behalf of every consumer — retries with idempotency keys, pagination iterators, typed errors, timeouts — and how it is built to survive the API evolving underneath it. A strict SDK turns your safe changes into their crashes.

Q · What should the client library do for every consumer — and how must it be built so the API can keep evolving underneath it?
Performance & Observability

The API-shaped levers: payload size, compression, request count, caching. Request IDs, metrics without high-cardinality labels, logs that never contain tokens, and contract tests.

API Performance: The Levers You Actually Own
▶ lab

Most API latency is decided by the contract, not the code: how many round trips a task needs, how many bytes each carries, and how often a request can be skipped entirely. The levers are payload, compression, request count, caching, serialization and field selection.

Q · Which parts of this API's latency are contract decisions, and which lever pays back the most for the consumers who feel it?
Payload Size: 20KB, 200KB, 5MB
▶ lab

Payload cost is paid four times — transfer, serialization, memory, client parse — and it scales with every caller. A 20KB response is a non-event, 200KB is a tax on every mobile render, and 5MB is an architecture mistake wearing a JSON costume.

Q · What does each response size cost across the four places it is paid, and where should the contract cap it?
Compression: Cheaper Bytes, Not Fewer

gzip or brotli shrinks JSON 5–10× for a CPU price paid on every request. The trade inverts on small payloads, already-compressed data and CPU-bound services — and the negotiation headers are contract clauses, not transport trivia.

Q · For this endpoint's payload sizes, consumers and traffic, does trading CPU for bandwidth pay — and what does the contract promise about negotiation?
Request IDs: The Contract's Correlation Clause

One opaque id, minted at the edge, propagated through every hop, returned in every response — especially errors. It is the difference between "can you send a screenshot?" and finding the exact failing request in one query.

Q · When a consumer reports one failed call, can both sides find that exact request across every hop it touched?
API Metrics: Rate, Errors, Duration, Sizes

Four signals per endpoint — request rate, error rate by class, duration percentiles, payload sizes — labeled by route template, method and status class. The craft is in the labels: one high-cardinality label like user_id can melt the metrics system that was supposed to watch everything else.

Q · Can you answer "is this endpoint healthy, for whom, and compared to what we promised?" from metrics alone — without grepping logs?
API Logging Without Leaking

One structured line per request: operation, status, duration, request id, principal, safe context. The hard part is the discipline of absence — no tokens, no passwords, no full bodies — because logs are the widest-read, longest-retained copy of your traffic.

Q · Does every request leave exactly one useful, queryable record — and is it impossible for that record to contain a credential?
Testing the Contract, Not Just the Code

Unit tests prove the handler works; contract tests prove the promises hold; compatibility tests prove yesterday's consumers survive tomorrow's deploy. An API test suite is organized around the guarantees, and the cheapest test that catches each broken guarantee wins.

Q · Which promise does each test protect — and would this suite catch a change that breaks a consumer before the consumer does?
The Gateway as Policy Boundary

Authentication verification, rate limits, size caps, request IDs, TLS, routing and version steering can live at the gateway — one enforcement point instead of N reimplementations. The discipline is knowing which contract clauses belong at the edge, and remembering that gateway-generated responses are part of your contract too.

Q · Which clauses of this contract should be enforced once at the edge, which must stay in the services — and do the gateway's own responses honor the contract?