REST API Design
Design APIs that behave predictably, before you write a single line of server code
Category: Backend β API Design Principles
Learning Path: What β Why β Learning Modules β Production Example β Interview Prep
Before you start: having built at least one basic API β this academy's own Express technology covers exactly that β makes this course far more concrete, since you'll have something real to map every principle onto. This is not another "how to write server code" course: it's a design/principles course. Every idea here applies regardless of language or framework β Node, Python, Go, Java, whatever eventually implements it. If you haven't built anything yet, Express first, then come back; the principles will click faster with a real API to point them at.
What is REST API Design?
Think of HTTP methods and URLs as a shared vocabulary between systems, the same way traffic signals are a shared vocabulary between drivers. A driver who has never met another driver still knows red means stop and green means go β not because they coordinated in advance, but because both follow the same convention. REST works the same way: a client that has never talked to a particular API before can still guess, correctly, that GET /users/42 fetches user 42 and DELETE /users/42 removes it β because both sides agree on what GET and DELETE mean, independent of what the API actually does internally.
REST (REpresentational State Transfer) is an architectural style, not a protocol or a formally specified standard the way GraphQL or SOAP are. There's no REST governing body and no REST compliance test β it's a set of conventions and constraints, originally described by Roy Fielding in his 2000 doctoral dissertation, built entirely on top of HTTP as it already existed. The core idea: a URL identifies a resource β a noun, like /users/42 or /orders β and the HTTP method expresses the action β a verb, like GET (read), POST (create), PUT/PATCH (update), DELETE (remove). Keep nouns in the URL and verbs in the method, and an API's shape starts telling you what it does before you've read a line of documentation.
Why REST API Design?
Without shared conventions, every API is a bespoke puzzle a consumer has to learn from scratch β one API uses /getUser?id=42, another uses /user/fetch/42, a third returns 200 OK for a failed request with the real error buried in the JSON body. None of that is technically "wrong" in the sense of breaking HTTP, but every inconsistency is something a client developer now has to read documentation (or, worse, guess) to work around. That's slow, error-prone, and doesn't scale past a handful of integrations.
REST's conventions, followed consistently, make an API's behavior predictable from its shape alone. A developer who's used one well-designed REST API can walk into another and correctly guess most of its behavior on sight β same verbs, same status code ranges, same general URL patterns. That predictability is the actual value, not "REST is more efficient" or "REST is required by some spec" β it isn't, and alternatives (GraphQL for flexible client-driven queries, gRPC for high-performance internal service-to-service calls) genuinely fit some problems better. But for public and general-purpose APIs specifically, REST remains the dominant real-world style precisely because so many developers already share its vocabulary.
Learning Modules
Module 01 β Resources, URLs & HTTP Methods
Nouns in URLs, verbs in methods β the foundation everything else builds on
Topics covered:
/users not /getUsers) β π’ Beginner/users/42/orders) β π‘ IntermediateModule 02 β Status Codes & Response Design
What each status code range actually promises the client
A status code is a contract, not decoration β a client's retry logic, error handling, and caching behavior all key off it. Getting the range right (2xx succeeded, 4xx the client's request was wrong, 5xx the server failed) is the single highest-leverage design decision in the whole API, because it's the one thing every client depends on being correct without reading your documentation first.
Topics covered:
Module 03 β Versioning, Pagination & Idempotency
Designing an API that can change without breaking the clients already using it
Topics covered:
Production Example
Interview Prep
PSR Formula: Answer every question: Problem β Solution β Result. 45-90 seconds max.
Common Interview Questions
Q1. What makes an API "RESTful," concretely β not just "it uses JSON over HTTP"?
A: Problem: "JSON over HTTP" is commonly (and incorrectly) used as a synonym for REST, which obscures what's actually being evaluated in a design review. Solution: RESTful means resources are addressed as nouns in URLs, HTTP methods express the action, status codes accurately reflect outcome, and (in the fuller sense Fielding described) responses are stateless and cacheable. Result: an API can serve JSON over HTTP and still not be RESTful β e.g. an API using only POST for every operation, with actions encoded in the body ({"action": "delete"}) β the format is JSON/HTTP, but the design isn't following REST's conventions at all.
Q2. PUT vs. PATCH β what's the actual difference and why does it matter?
A: Problem: the two are frequently used interchangeably in practice, which causes real bugs. Solution: PUT means "replace this resource entirely with what I'm sending" β any field not included is treated as cleared/defaulted. PATCH means "apply this partial change" β only the included fields change. Result: using PUT for what should be a partial update can silently wipe fields the client didn't intend to touch; this is a genuinely common, concrete production bug, not a pedantic distinction.
Q3. Why does idempotency matter for API design, specifically around retries?
A: Problem: networks fail β a client can send a request, lose the response to a timeout, and have no way to know if the server actually processed it. Solution: if the operation is idempotent (GET, PUT, DELETE β calling it N times has the same effect as calling it once), the client can safely retry. If it isn't (a bare POST creating a new order), a naive retry risks a duplicate. Result: this is why POST endpoints for financial/critical operations often require an explicit idempotency key, so a retried request with the same key is recognized and deduplicated server-side rather than creating a second charge.
Q4. Explain the main HTTP status code ranges and the distinction within 2xx (200 vs 201 vs 204).
A: Problem: status codes are how a client detects success/failure programmatically, so getting the range wrong breaks that detection. Solution: 2xx = success, 3xx = redirect elsewhere, 4xx = the client's request was wrong, 5xx = the server failed. Within 2xx: 200 is a generic success with a body, 201 specifically means "created" (used after POST, typically with a Location header pointing at the new resource), 204 means success with no body to return (common after DELETE). Result: picking 201 over 200 after a create isn't cosmetic β clients and tooling (some auto-follow Location) rely on the distinction.
Q5. 401 vs 403 β what's the actual difference?
A: Problem: both are "you can't do that," commonly conflated. Solution: 401 Unauthorized actually means unauthenticated β no valid credentials were presented at all. 403 Forbidden means the credentials are valid, but the authenticated identity doesn't have permission for this specific action. Result: returning 401 when a logged-in user tries to access another user's private data (instead of 403) leaks information about whether authentication itself is the problem β a subtle but real API-design/security detail.
Q6. How would you design pagination for an endpoint returning potentially millions of rows?
A: Problem: returning everything at once is both a performance problem for the server and an unbounded-growth problem for the client. Solution: offset-based pagination (?page=3&limit=20) is simple but degrades on large datasets (the database still has to scan/skip prior rows) and can skip/duplicate items if rows are inserted mid-pagination; cursor-based pagination (?cursor=abc123) uses a stable pointer into the dataset, avoiding both problems, at the cost of not supporting "jump to page 50" directly. Result: for a large, frequently-mutating dataset, cursor-based pagination is the more robust default; offset is fine for small, stable, or admin-only datasets.
Q7. How should an API handle versioning, and what's the tradeoff between URL-path and header-based versioning?
A: Problem: an API will eventually need a breaking change, and existing clients can't be forced to update instantly. Solution: URL-path versioning (/v1/users, /v2/users) is simple, visible, and cacheable by URL, but "pollutes" the URL and can tempt teams into treating every change as a new major version. Header-based versioning (Accept: application/vnd.api+json;version=2) keeps URLs stable but is less discoverable and harder to test with a browser or a simple curl. Result: URL-path versioning is the more common real-world default specifically because of its visibility and simplicity for consumers, even though header-based is arguably more "correct" REST.
Q8. What does 409 Conflict actually indicate, and give a concrete example.
A: Problem: teams sometimes reach for 400 for anything that isn't a clean success, losing useful distinctions. Solution: 409 specifically means the request is valid, but conflicts with the current state of the resource β e.g. trying to create a user with an email that already exists, or two concurrent updates to the same resource with optimistic-locking version mismatch. Result: returning 409 (not 400) for this case lets a client's retry logic distinguish "my request itself was malformed" from "my request was fine, but the world changed underneath it" β genuinely different recovery strategies.
Q9. REST vs. GraphQL β what's the actual tradeoff, not just "GraphQL is newer"?
A: Problem: framed as a strict upgrade, this misses real tradeoffs in both directions. Solution: REST's fixed resource shapes are simple to cache (by URL) and reason about, but can lead to over-fetching (getting fields you don't need) or under-fetching (needing several requests to assemble one view). GraphQL lets the client specify exactly what fields it needs in one request, avoiding both, but at the cost of harder HTTP-level caching and a more complex server implementation. Result: REST tends to fit public, general-purpose APIs well; GraphQL tends to fit client-driven apps with complex, varying data needs (e.g. a mobile app assembling one screen from many resource types) better.
Q10. How would you design a consistent error response shape across an entire API?
A: Problem: without a deliberate standard, different endpoints (often written by different people over time) return errors in different shapes, forcing every client integration to write bespoke error-parsing per endpoint. Solution: define one error envelope used everywhere β e.g. { "error": { "code": "STRING_CODE", "message": "human-readable", "field": "optional, for validation errors" } } β paired with the correct status code, and enforce it at the framework/middleware level rather than per-handler. Result: a client can write one generic error handler for the entire API instead of one per endpoint, and the machine-readable code (not just message, which might change wording) is what client code should actually branch on.
Official Resources
Try It (2 Minutes)
For each scenario, write out the URL, HTTP method, and expected success status code β no server needed, just design it on paper:
991) from a post (id 42).(Answers: 1 β GET /posts?page=2&limit=10, 200. 2 β DELETE /posts/42/comments/991, 204. 3 β POST /users, 409 Conflict, not 400 β the request was well-formed, it just conflicts with existing state.)

