REST API Design — Advanced
API versioning strategies — URL-path vs. header-based
An API will eventually need a breaking change — a field renamed, a response shape restructured — and existing clients can't be forced to update the instant that happens. Versioning is how an API ships a breaking change without breaking every consumer at once.
URL-path versioning — the version lives directly in the URL:
Simple, visible at a glance, trivially cacheable by URL, and easy to test with a bare curl or browser. The tradeoff: it "pollutes" every URL in the API, and teams sometimes over-use it — bumping a major version for a change that could have been additive instead.
Header-based versioning — the URL stays stable, the version is negotiated via a header:
Keeps URLs clean and arguably more "correct" from a pure-REST standpoint (a resource's identity/URL shouldn't change just because its representation format did), but it's less discoverable — a developer can't tell the version from the URL alone, and it's harder to test casually.
In practice, URL-path versioning is the more common real-world default specifically because of that visibility and ease-of-use for consumers — most public APIs (Stripe, GitHub, Twilio, among many others) use it, even though it's arguably the less "pure" REST approach (needs verification — recheck against current source; specific companies' exact versioning schemes can change).
Designing for backward compatibility — safe vs. breaking changes
Not every change requires a new version. The distinction that matters:
A well-behaved client is expected to ignore fields it doesn't recognize — which is exactly what makes adding a new field safe, and removing/renaming one dangerous: any client that was reading that field breaks the moment it's gone, with no warning. The practical discipline: treat every response shape as a contract, and audit any proposed change against "would an existing client, written against the current shape, break?" before shipping it without a version bump.
Rate limiting design
A rate limit protects the API from being overwhelmed by any single client (deliberately or accidentally) — and needs to communicate its state clearly enough that well-behaved clients can back off correctly rather than hammering a failing endpoint.
429 is the specific status code for this — not 403 (which implies a permissions problem, not a temporary throttle) and not 503 (which implies the server is overloaded, a different failure mode). Retry-After tells a well-behaved client exactly how long to wait before retrying, which is the actual point of the design — without it, a client is left guessing and may retry immediately, making the overload worse rather than better.
Authentication/authorization at the API-design level
This course covers authentication and authorization only at the design-decision level — where credentials go, and what status code an API returns for each failure mode. This platform's dedicated Authentication technology (later in this academy) covers actual implementation depth: sessions, JWTs, OAuth flows, and how to wire them into a real server.
At the design level, the decisions that matter:
Authorization header (Authorization: Bearer ), never as a URL query parameter — URLs end up in server logs, browser history, and Referer headers, all of which leak a token that shouldn't be logged anywhere.Getting 401 vs. 403 right matters for a client's error handling — 401 usually means "re-authenticate" (redirect to login), 403 means "this identity, correctly authenticated, simply can't do this" (no re-login will fix it).
Error response body design — one consistent shape across the entire API
The single highest-leverage design decision for API error handling: define one error envelope, used by every endpoint, rather than letting each handler invent its own shape.
Key design choices worth being deliberate about:
code, not just a human-readable message — client code should branch on code (stable, versioned deliberately), never on message text (which can change wording without warning and silently break string-matching client code).{ "error": { ... } }) — enforced at the framework/middleware level, not left to individual handlers, so a client can write one generic error parser for the whole API instead of one per endpoint.field, when relevant, pointing at exactly which input caused a validation failure — genuinely useful for form-validation UIs mapping API errors back to specific inputs.Try It (2 Minutes)
An existing GET /orders/42 endpoint currently returns { "id": 42, "total": 99.99 }. Design the change to add a currency field to every order response — decide whether this requires a version bump, and justify the answer using the safe-vs-breaking distinction above.

