REST API Design Practices

API Types & Design — REST API Design Practices
API Types & Design
Chapter 4 · REST API Design Practices

🛠️ REST API Design Practices

Chapter 3 covered REST's architectural theory. This chapter is the practical companion — the concrete decisions every real REST API has to make. Several of these have already shown up as brief asides in the framework courses on this site (Express, FastAPI, Rails, Laravel, Django); this is the first time they get a proper, dedicated treatment on their own.

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.

StrategyExamplePros / Cons
URL versioning/v1/users, /v2/usersMost visible and simplest to reason about; "impure" since a resource's URL technically shouldn't change just because its representation did
Header versioningAccept: application/vnd.api+json;version=2Keeps the URL stable and is arguably "purer" REST; much less visible/discoverable to developers browsing the API
Query parameter versioning/users?version=2Easy 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

GET /orders?status=shipped&sort=-createdAt&limit=10

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

POST /payments HTTP/1.1 Idempotency-Key: 6f9a1e2c-... Content-Type: application/json { "amount": 4999, "currency": "usd" }

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:

HeaderMeaning
X-RateLimit-LimitThe maximum requests allowed in the current window
X-RateLimit-RemainingHow many requests are left in the current window
X-RateLimit-ResetWhen 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.

→ Solution

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.

→ Solution

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.

→ Solution

⚠️ Gotcha: Offset Pagination Skipping or Duplicating Rows

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.