REST API Design Practices
🛠️ REST API Design Practices
Versioning Strategies
APIs change over time, but existing clients can't be forced to update the instant something breaking ships. Versioning lets old and new behavior coexist.
| Strategy | Example | Pros / Cons |
|---|---|---|
| URL versioning | /v1/users, /v2/users | Most visible and simplest to reason about; "impure" since a resource's URL technically shouldn't change just because its representation did |
| Header versioning | Accept: application/vnd.api+json;version=2 | Keeps the URL stable and is arguably "purer" REST; much less visible/discoverable to developers browsing the API |
| Query parameter versioning | /users?version=2 | Easy to add on top of an existing unversioned API; easy to forget or omit accidentally |
URL versioning is by far the most common in practice — it's the most visible option, and visibility usually wins over architectural purity.
Pagination
Returning every row of a 2-million-row table in one response isn't practical for the server or the client. Two common patterns solve this differently:
Offset/Limit vs. Cursor-Based Pagination
Offset/Limit (Page-Based)
GET /products?offset=20&limit=20
Simple to implement and reason about, but if items are added or removed while a client is paging through results, rows can be skipped or duplicated between pages.
Cursor-Based
GET /products?after=abc123&limit=20
The cursor points to a specific position in the data itself (not just a numeric offset), staying stable even as the underlying data changes — the pattern used by production APIs like Stripe and GitHub.
Filtering & Sorting via Query Parameters
Query parameters are the conventional place for narrowing down or reordering a resource collection:
A Filtered, Sorted Request
Reads as: "give me orders where status is shipped, sorted by createdAt descending (the leading - conventionally means descending), limited to 10 results."
Idempotency in Practice
Chapter 2 introduced idempotency as a property of HTTP methods. Here's where it becomes a real, practical problem: a client sends POST /payments, the connection times out before a response arrives, and the client — reasonably — retries. Did the payment actually go through the first time? If it did, the retry risks creating a second charge, since POST isn't idempotent by design.
The common real-world fix is an Idempotency-Key header: the client generates a unique key (typically a UUID) once, and sends the same key on both the original request and any retry:
An Idempotency-Key in Action
The server remembers which idempotency keys it has already processed; if the same key arrives again, it returns the original result instead of creating a second payment. This is exactly the pattern Stripe's API popularized.
Rate Limiting
Rate limiting caps how many requests a client can make in a given time window, protecting server infrastructure and keeping usage fair across clients. Rate-limited APIs conventionally return the 429 Too Many Requests status introduced in Chapter 2, along with headers describing the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The maximum requests allowed in the current window |
X-RateLimit-Remaining | How many requests are left in the current window |
X-RateLimit-Reset | When the window resets and the count starts over |
Rate limiting also plays a security role, not just a capacity-management one — the Authentication & Session Security course's login-attacks chapter (bc1-3) covers rate limiting specifically as a brute-force defense, the same underlying mechanism applied to a different problem.
💻 Coding Challenges
Challenge 1: Choose a Versioning Strategy
Recommend a versioning strategy for (a) a public API with thousands of external consumers who don't read documentation closely, and (b) an internal API used only by one team who reviews every deploy together. Justify each choice.
Goal: Practice matching a design decision to its actual audience rather than picking one "correct" universal answer.
Challenge 2: Design a Paginated Response
A /products resource has 10,000 items and is updated constantly (items added/removed throughout the day). Design the query parameters and response shape for cursor-based pagination through it, and explain why cursor-based fits better than offset/limit here.
Goal: Practice applying the offset-vs-cursor tradeoff to a concrete, changing dataset.
Challenge 3: Fix a Non-Idempotent Retry Bug
A client's POST /payments request times out, the client retries automatically, and the customer ends up charged twice. Propose a fix using this chapter's Idempotency-Key pattern, describing what the client and server each need to do.
Goal: Practice applying the idempotency-key pattern to a real double-charge scenario.
Offset/limit pagination looks harmless until data changes mid-pagination. Say a client fetches ?offset=0&limit=20, then a new row is inserted at the very front of the dataset, and the client fetches ?offset=20&limit=20 next — that new insertion just shifted everything over by one position, so the client's second page now starts one row too early, silently duplicating the last row of page one. The reverse happens with a deletion near the front: a row gets silently skipped entirely. Cursor-based pagination avoids this specific failure mode because the cursor tracks a stable position in the data itself, not a numeric offset that shifts every time the underlying rows change.
🎯 What's Next
The next chapter steps away from REST entirely to cover SOAP APIs — XML envelopes, WSDL contracts, and why plenty of enterprise, banking, and government systems still rely on this older, stricter style.