REST constraints and resource modeling, versioning strategies, cursor vs offset pagination, OpenAPI 3.0 spec, Swagger UI — designing APIs that don't need a changelog.
Module P-10 — REST API Design and OpenAPI Documentation
What this module covers: A well-designed API is a product. Developers who consume it should be able to predict its behaviour, discover its capabilities, and upgrade to new versions without their code breaking. This module covers the REST constraints that produce predictable APIs (including HATEOAS, the constraint most APIs skip), resource modelling, versioning strategies and their trade-offs, offset vs compound cursor pagination, the HTTP status codes that actually matter, RFC 7807 Problem Details and rate-limit response headers, idempotency keys for safely retrying mutations, and generating interactive OpenAPI 3.0 documentation from your Express routes with Swagger UI.
REST Constraints: What Makes an API RESTful
REST (Representational State Transfer) is not a standard — it is a set of architectural constraints defined by Roy Fielding. An API that follows them is predictable and easier to consume.
The six constraints, and what they mean in practice:
1. Uniform interface — a consistent way to interact with all resources. In practice: use HTTP methods semantically, use nouns not verbs in URLs, use standard status codes.
2. Stateless — each request contains all information needed to process it. No session state stored on the server between requests. Authentication via token (not session cookie) follows this constraint.
3. Client-server separation — the client and server evolve independently. Your mobile app and your API can be deployed separately.
4. Cacheable — responses must declare whether they can be cached. Use Cache-Control, ETag, and Last-Modified headers.
5. Layered system — the client doesn't know if it's talking to the actual server or a proxy/load balancer.
6. Code on demand (optional) — servers can send executable code to clients (e.g., JavaScript). Rarely used in APIs.
Most "REST APIs" only follow constraints 1–3. That's fine. What matters practically is the uniform interface.
HATEOAS: The Part of "Uniform Interface" Most APIs Skip
Constraint 1 actually has several sub-constraints in Fielding's original definition, and the one almost every "REST API" skips is HATEOAS — Hypermedia As The Engine Of Application State. The idea: a response includes links describing what the client can legally do next, so the client discovers valid transitions from the response itself instead of relying on out-of-band knowledge (documentation memorized in advance) of the API's URL structure.
A client that understands _links doesn't hardcode "orders can be cancelled by POSTing to /orders/{id}/cancel" — it checks whether a cancel link is present on this order (an already-shipped order's response simply wouldn't include one) and follows it if present. That's what turns an API into a hypermedia-driven state machine instead of a fixed contract the client has memorized in advance.
In practice, almost no public REST API implements this fully — Stripe, GitHub, and most others document fixed URL patterns instead, because generic HATEOAS clients are rare and the tooling ecosystem (OpenAPI included) is built around fixed, documented paths rather than discovered ones. It's still worth naming explicitly: when people say an API "isn't really RESTful," HATEOAS is almost always the specific constraint they mean, even when they don't use the term.
Resource Modelling: URLs Are Nouns
URLs identify resources. HTTP methods express what to do with them.
Nested resources — when a resource only makes sense in the context of another:
Limit nesting depth — more than two levels becomes unwieldy. /users/42/posts/7/comments/3/likes is hard to read and hard to maintain. At that depth, consider a top-level resource: GET /comments/3/likes.
Actions that don't fit CRUD — some operations are not naturally resource-oriented. Use sub-resources with a verb:
These are acceptable when the alternative (overloading PATCH) would be unclear.
HTTP Methods and Idempotency
| Method | Idempotent | Safe | Use for |
|---|---|---|---|
| GET | ✓ | ✓ | Read — never modify state |
| HEAD | ✓ | ✓ | Like GET but no body — check existence/headers |
| POST | ✗ | ✗ | Create, or actions with side effects |
| PUT | ✓ | ✗ | Full replacement — same result if called multiple times |
| PATCH | ✗ | ✗ | Partial update |
| DELETE | ✓ | ✗ | Delete — second call returns 404, not an error |
Idempotent: calling the operation N times produces the same result as calling it once. Idempotency enables safe retries — crucial for unreliable networks. Design your API to support this: DELETE returning 404 on a second call is correct, not an error.
PATCH vs PUT:
For most APIs, PATCH is the right choice for updates — PUT requires the client to know the full current state.
Idempotency Keys: Making POST Safe to Retry
The table above marks POST as neither idempotent nor safe — correctly, since POST usually creates a new resource, and calling it twice normally means two resources. That's a real problem the moment a client needs to retry a POST after a dropped connection: it has no way to tell whether the original request succeeded and only the response was lost in transit, or whether nothing happened at all.
The fix is a client-supplied Idempotency-Key header — a UUID the client generates once per logical operation and resends unchanged on every retry of that same operation:
The server's contract: the first request with a given key executes normally and its response is cached against that key; every subsequent request with the same key gets the original response replayed, without re-executing the underlying mutation. (The full middleware — a thin Express layer backed by Redis — is built out in Module P-7's "Idempotency Keys for Client-Retried Mutations" section; this is the same mechanism, described here from the API-design side rather than the implementation side.)
Production story: a UPI merchant integration had a customer's flaky mobile network drop the response to POST /payments after the transaction had already cleared on the backend. The client's retry logic, seeing no response, resubmitted the identical request. Without an Idempotency-Key, the API had no way to recognize the second POST as a retry of the first rather than a brand-new charge — it processed both, and the customer was debited twice. The fix wasn't a new feature so much as a new requirement: document Idempotency-Key as mandatory on /payments and reject requests that omit it.
HTTP Status Codes That Matter
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion