Representational State Transfer
Representational State Transfer (REST) is an architectural style — not a protocol, not a standard, not a wire format — for distributed hypermedia systems, derived by Roy Fielding in Chapter 5 of his 2000 University of California, Irvine PhD dissertation Architectural Styles and the Design of Network-based Software Architectures. Fielding constructed REST not by inventing something new but by naming the constraints that already made the World Wide Web work — he was simultaneously a co-author of the Hypertext Transfer Protocol (HTTP) 1.1 specification, and the dissertation is best read as a retrospective explanation of why HTTP succeeded as a planet-scale architecture. The style is defined by six constraints layered on top of each other (client-server separation, statelessness, cacheability, uniform interface, layered system, and the optional code-on-demand), each of which trades some flexibility for a property the Web depended on at scale: independent evolvability, intermediary visibility, and a single uniform contract that tools, caches, and proxies could understand without per-application configuration. In industry the term has been so thoroughly diluted that “REST API” today usually means any HTTP-plus-JavaScript Object Notation (JSON) interface that uses HTTP methods semantically — Leonard Richardson’s Maturity Model (2008) puts most of the industry at level 2 of 4, missing the hypermedia (Hypermedia As The Engine Of Application State, HATEOAS) constraint that Fielding considered non-negotiable. This note treats both REST as Fielding defined it and “REST” as the industry actually ships it, and is honest about the gap between the two.
1. Plain-Language Definition
REST is the answer to the question: if every application on the planet has to talk to every other application over HTTP, what rules must they follow so that the network of caches, proxies, content-delivery networks, and mobile clients between them can do useful work without understanding any specific application? Fielding’s insight is that the Web’s architectural success — its ability to scale from a few CERN documents in 1990 to today’s tens of billions of devices — depended on a small set of universal contracts: every “thing” has a stable identifier, every interaction is a self-describing message that any intermediary can inspect, the same five-or-so verbs apply to everything, the server holds no per-client conversational state between requests, and the client doesn’t need a precompiled map of the server’s URLs because the server hands them out as part of its responses. Removing any one of those constraints regresses some Web property: removing self-description breaks caching, removing statelessness breaks horizontal scaling, removing the uniform interface forces every intermediary to know every application.
Pragmatically, when an engineer says “we built a REST API,” they usually mean: HTTP methods (GET, POST, PUT, PATCH, DELETE) carry resource semantics; Uniform Resource Identifiers (URIs) name resources rather than actions (/users/42 not /getUser?id=42); HTTP status codes indicate outcome; the wire format is JSON; pagination, filtering, and sorting are query parameters. That is roughly Richardson’s level 2 — sometimes called “HTTP-flavored Remote Procedure Call (RPC)” by purists, who reserve the term REST for the level-3 hypermedia case. In this note, “REST” without further qualification means the industry-shipped style; “Fielding-pure REST” means the level-3 dissertation definition.
2. Mechanism — The Six Constraints, In Detail
Fielding derives REST by starting from the empty null style (no constraints, total flexibility) and adding constraints one at a time, each trading off some property. The order matters: each layer presupposes the prior ones.
2.1 Client-Server Separation
The first constraint is the Client-Server Architecture: a client process initiates requests, a server process owns the resource and replies. The motivation is separation of concerns — the user-interface code (client) evolves independently of the storage and business-logic code (server). A web browser written in 1995 still works against a server written in 2026 because the contract between them is not a shared library but a wire protocol. This is what made the Web’s heterogeneity possible: tens of distinct browsers, tens of thousands of distinct servers, all interoperating because none of them share code.
2.2 Statelessness
Each request from client to server must contain all the information necessary for the server to understand it, with no stored client context on the server between requests. The session, if any, lives on the client (the browser holds the cookie; the mobile app holds the access token). The server treats every request in isolation.
The benefit is enormous and enables nearly everything that follows: any server in a fleet can answer any request, so load balancing is trivial (round-robin works); a server crash loses no conversational state; horizontal scaling is additive. The cost is that requests are larger (every request re-sends the credential, the context, the resource identifier) and that some inherently stateful workflows (multi-step wizards, transactions spanning calls) become awkward. The standard escape hatch — Cookie and Authorization headers — does store identity on the client and reauthenticates per request, which is statelessness preserved.
There is a subtle distinction between application state (which step of a checkout you’re on — properly the client’s job in REST) and resource state (the order’s current status in the database — properly the server’s job, durably stored). Fielding’s constraint forbids the server from holding application state between requests; it has nothing to say about resource state, which is what databases are for.
2.3 Cacheability
Responses must explicitly declare themselves cacheable or not (via headers like Cache-Control, ETag, Expires, Last-Modified, all defined in RFC 9111). When a response is cacheable, an intermediary (the browser cache, a Content Delivery Network (CDN) edge node, a corporate forward proxy) may serve identical subsequent requests from the cache without contacting the origin server. The intermediary doesn’t need to understand the application — Cache-Control: public, max-age=300 is universal vocabulary.
This constraint is what made the Web cheap. Without caching, every page view would hit the origin; with HTTP-level caching, a typical web page’s static assets are served from edges 95%+ of the time. APIs that fail to set cache headers (or set them too defensively) leave that performance on the floor.
2.4 Uniform Interface — The Heart of REST
The uniform interface is the most distinctive constraint and itself decomposes into four sub-constraints. It trades efficiency in the small (an interface designed precisely for one application would be terser) for generality at scale (any tool, written by anyone, can interact with any RESTful service).
(a) Identification of resources via URIs. Every “thing” the system exposes — a user, a comment, a search-result page, a calculation — has a Uniform Resource Identifier (URI). The URI names the resource; it does not name an action. /users/42 names “user 42”; /users/42/orders names “the orders of user 42.” The URI is opaque to the client in Fielding-pure REST — clients should not parse them or construct new ones from templates; they should only follow ones the server hands out (see §2.4(d)).
(b) Manipulation of resources through representations. When the client retrieves /users/42, the server returns a representation of the user — JSON, Extensible Markup Language (XML), HTML — chosen by content negotiation (Accept header). The representation is a snapshot, not the resource itself. The client modifies the resource by sending a new representation back, typically with PUT or PATCH. The same resource can have multiple representations; Vary: Accept lets caches keep them straight.
(c) Self-descriptive messages. Each message contains enough information for a generic recipient to process it: standard HTTP verbs, standard status codes, a Content-Type declaring the media type, cache-control directives, link relations. Crucially, the application-specific schema is named by the media type (application/vnd.github.v3+json, application/hal+json, application/vnd.api+json), so an intermediary that understands the generic format can route, cache, and log without knowing the application. The client does not need a separately distributed Interface Description Language (IDL) file to interpret the response; the response describes itself.
(d) Hypermedia As The Engine Of Application State (HATEOAS). This is the constraint Fielding considered the defining property of REST and the one most often missing in practice. The idea: the client begins from a single well-known entry-point URI and from there is handed — by the server, in response bodies — the URIs and link relations for the next legitimate transitions. The client does not have a precompiled list of endpoints; it discovers them dynamically.
A non-HATEOAS response: {"id": 42, "name": "Ada", "balance": 1000} — to do anything else with this account, the client must consult out-of-band documentation telling it that POST /accounts/{id}/transfer exists.
A HATEOAS response: {"id": 42, "name": "Ada", "balance": 1000, "_links": {"self": {"href": "/accounts/42"}, "transfer": {"href": "/accounts/42/transfer", "method": "POST"}, "freeze": {"href": "/accounts/42/freeze", "method": "POST"}}} — the client now knows what it can do next from the data itself. If the server later renames the endpoint or restricts when freeze is permitted (omitting the link when frozen), the client adapts without a redeploy.
Fielding’s 2008 blog post (REST APIs must be hypertext-driven) was an exasperated correction: he had watched the industry adopt the term “REST” while abandoning HATEOAS, and wrote “if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful.” Industry, in turn, has mostly continued to call its level-2 APIs RESTful anyway.
2.5 Layered System
The architecture is composed of hierarchical layers so that each component cannot see beyond the immediate layer it interacts with. Clients cannot tell whether they are talking to the origin server, a load balancer, a reverse proxy, an API Gateway, or a CDN — the contract is the same. This is what allows transparent intermediaries: a service mesh sidecar can be inserted between client and server without either party knowing, because the layered constraint guarantees the sidecar can act as if it were the server (from the client’s perspective) and as if it were the client (from the server’s perspective).
2.6 Code-On-Demand (Optional)
The only optional constraint. Servers may extend client functionality by sending executable code — JavaScript in browsers, Java applets historically. This loosens visibility (an intermediary cannot understand the dynamic behavior the code adds) but enables progressive enhancement. Most non-browser REST APIs ignore this constraint entirely.
3. Origins — How REST Got Named
REST did not exist in 1995 when the Web was already running; it existed implicitly in the design of HTTP/1.0 and the browser–server interaction, but had no name. Roy Fielding spent roughly 1996–2000 doing dissertation research that asked: what architectural style does the Web have, and which of its properties are essential to its scaling? The answer was REST. Chapter 5 of the dissertation derives it constraint-by-constraint; Chapters 4 and 6 contrast it with alternatives (the Distributed Objects style of CORBA — Common Object Request Broker Architecture — and the Mobile Code style of applets) and argue REST’s superiority for web-scale workloads.
The popularization happened in two waves. Leonard Richardson and Sam Ruby’s RESTful Web Services (2007) translated the dissertation’s vocabulary into pragmatic HTTP-and-JSON design rules — the “resource-oriented architecture” — and is the book that gave most working programmers their REST mental model. Then Richardson’s QCon 2008 talk introduced the maturity model that became the universal reference for “how RESTful is your API?“
4. The Richardson Maturity Model
Richardson’s four levels, popularized further by Martin Fowler’s 2010 explainer, describe a ladder of increasing REST conformance.
Level 0 — The Swamp of POX (Plain Old XML). A single endpoint takes all requests as POST and the body specifies the operation. SOAP — Simple Object Access Protocol — lives here, as do many legacy “web services”: one URL, one HTTP method, the verb is buried in the payload. Effectively HTTP is being used as a tunnel for RPC.
Level 1 — Resources. The API now has multiple URIs naming distinct resources (/users/42, /orders/77) instead of one mega-endpoint. This already buys logging, caching, and routing benefits because intermediaries can distinguish resources. But every operation is still a POST with a body; HTTP methods are not yet semantic.
Level 2 — HTTP Verbs. The API uses GET for safe reads, POST for non-idempotent creates, PUT for idempotent replaces, DELETE for removes; status codes are semantic (404 vs 200, 409 vs 400). This is where most production “REST APIs” live — Stripe, GitHub, Twitter, Shopify Admin, the AWS service APIs, Kubernetes’ own API. Level 2 captures most of the practical wins of REST: cacheable reads, idempotent retries, generic intermediaries.
Level 3 — Hypermedia Controls (HATEOAS). Responses include link relations that drive the client through valid next transitions. The client begins at a root URI and follows links; URI structure becomes implementation detail of the server. Few production APIs reach this level. The clearest exemplars are PayPal’s payments API (HAL-formatted), Spring HATEOAS-based services, and the formats themselves — JSON:API, HAL, Siren, Collection+JSON.
Most "REST" is Not REST per Fielding
Fielding considers anything below level 3 not RESTful. In his 2008 blog post he wrote that an API not driven by hypertext “cannot be called REST.” Industry consensus is more permissive: level-2 designs are universally called REST and will be the assumed meaning when a job posting or interview asks about “REST APIs.” This note will use the looser definition unless explicitly stated.
5. Worked Example — A Blog API in REST
Let us design a small REST API for a blog with three resources — Posts, Comments, Authors — and walk through every level of the design.
5.1 Resource Identification (URI Design)
URIs are nouns, plural, hierarchical:
/authors collection of authors
/authors/{author_id} one author
/authors/{author_id}/posts posts written by that author
/posts all posts (across authors)
/posts/{post_id} one post
/posts/{post_id}/comments comments on that post
/posts/{post_id}/comments/{comment_id} one specific comment
Two design rules visible here. First, plural nouns even for the singleton case (/authors/42, not /author/42) — consistent and avoids special-casing the root collection. Second, hierarchies in URIs reflect ownership relationships (/posts/{id}/comments because a comment belongs to a post). For many-to-many relationships, prefer flat URIs with query parameters (/photos?album_id=...&tag=...) over forced hierarchies.
5.2 HTTP Verb Mapping
| Operation | Method | URI | Idempotent? | Safe? |
|---|---|---|---|---|
| List posts | GET | /posts?author=42&limit=20 | yes | yes |
| Read one post | GET | /posts/77 | yes | yes |
| Create post | POST | /posts | no | no |
| Replace post wholesale | PUT | /posts/77 | yes | no |
| Patch fields on post | PATCH | /posts/77 | depends | no |
| Delete post | DELETE | /posts/77 | yes | no |
Two semantic terms matter. Safe means the operation has no side effects observable to the user — GET and HEAD are safe; the others are not. Idempotent means doing it N times has the same observable effect as doing it once — GET, PUT, DELETE are idempotent; POST is not (two POSTs typically create two resources). Idempotency makes retry safe: if a PUT times out, the client can repeat it without fearing duplication. POST retries need application-level deduplication (an idempotency key header — Stripe popularized this pattern with Idempotency-Key).
PATCH, defined by RFC 5789, has muddier semantics. It denotes a partial update; whether it is idempotent depends on the patch format. JSON Merge Patch (RFC 7396) is idempotent (replace-with-this-subset is the same every time); JSON Patch (RFC 6902) with operations like {"op":"increment","path":"/likes"} is not.
5.3 Status Codes (Semantic Use)
200 OK GET succeeded; representation in body
201 Created POST created a new resource; Location: /posts/77
204 No Content DELETE or PUT succeeded; no body
301 Moved Permanently the resource has moved; Location: new URL
304 Not Modified conditional GET — client's cache is still fresh
400 Bad Request malformed body or query
401 Unauthorized no/invalid credentials (despite name, means unauthenticated)
403 Forbidden authenticated but not permitted
404 Not Found no such resource
409 Conflict state-machine violation (e.g., delete a non-empty thing)
410 Gone resource is deliberately removed and won't return
412 Precondition Failed conditional update (If-Match) failed — somebody beat you
422 Unprocessable Entity body is well-formed but semantically invalid
429 Too Many Requests rate limited; Retry-After header present
500 Internal Server Error server bug
502 Bad Gateway an upstream service is broken
503 Service Unavailable we're shedding load
504 Gateway Timeout an upstream took too long
Returning the right status code is what lets generic infrastructure (load balancers, monitoring, CDN edge logic) reason about the response without parsing the body.
For error response bodies, RFC 7807 defines a standard application/problem+json shape that has become the convention:
{
"type": "https://example.com/probs/insufficient-funds",
"title": "Insufficient Funds",
"status": 403,
"detail": "Your balance is $10 but the transfer is $50.",
"instance": "/accounts/42/transfers/abc"
}5.4 Pagination
Two dominant patterns, each with its trade-offs.
Offset–limit pagination is the obvious one: GET /posts?offset=200&limit=20. Simple, supports random access (jump to page 11), but breaks down at scale: an OFFSET 1000000 in the database scans and discards a million rows. Worse, results shift if rows are inserted between page fetches — page 5 ends with row R, page 6 begins with row R again because everything moved up.
Cursor-based pagination uses an opaque token from the prior page: GET /posts?after=eyJpZCI6Nzd9&limit=20. The cursor encodes a stable position (typically (timestamp, id) for tiebreaking) so subsequent pages skip past the last seen item rather than skipping a count. The trade-off: no random access (can’t jump to page 11 without paging through), but stable under concurrent writes and constant-time per page in the database. Stripe, Slack, GitHub use cursor pagination on their high-volume endpoints.
A response with hypermedia pagination links:
{
"data": [ ... ],
"_links": {
"self": { "href": "/posts?after=A&limit=20" },
"next": { "href": "/posts?after=B&limit=20" },
"first": { "href": "/posts?limit=20" }
}
}The next link being absent signals end of collection — the client iterates until it disappears.
5.5 Versioning
Three popular strategies, in rough order of REST-purity:
URL path versioning — /v1/posts, /v2/posts. Easy to read in logs; trivial to route at a gateway; violates the resource-identifier ideal (/v1/posts/77 and /v2/posts/77 denote the same logical resource via different URIs). Used by Twitter, Stripe (despite Stripe’s careful versioning model also using a date header for fine-grained revisions).
Custom media types — Accept: application/vnd.example.post.v2+json. The cleanest by Fielding’s lights: one URI per resource, the representation version is negotiated per request via content negotiation. Used by GitHub historically (application/vnd.github.v3+json).
Subdomain versioning — v1.api.example.com. Works at the DNS layer; useful when versions correspond to entirely separate deployments.
Stripe’s date-based versioning (Stripe-Version: 2024-04-10) is a fourth, hybrid approach: every API change is dated; a customer pins to a specific date and stays there. It pairs with a careful internal versioning system that translates between dated representations without forking the underlying service. This is currently the gold standard for long-lived public APIs.
5.6 HATEOAS in Action (Level 3)
A level-3 response to GET /posts/77:
{
"id": 77,
"title": "REST is hypertext-driven",
"body": "...",
"status": "published",
"author": { "id": 42, "name": "Ada" },
"_links": {
"self": { "href": "/posts/77" },
"author": { "href": "/authors/42" },
"comments": { "href": "/posts/77/comments" },
"edit": { "href": "/posts/77", "method": "PUT" },
"unpublish": { "href": "/posts/77/unpublish", "method": "POST" }
}
}The client’s state machine is now driven by which links the server returns. If the post is in draft, the server may include publish instead of unpublish. The client doesn’t need a static rule “draft posts have a publish endpoint” — it just renders buttons for whichever links appear. This is the property HATEOAS purchases: the server can rewrite its URL space and policy without breaking clients, because clients consume links rather than constructing them.
6. Variants and Surrounding Conventions
JSON:API (jsonapi.org) is a media type that prescribes response shape (a data/included/meta envelope, sparse fieldsets, sideloading of related resources) plus link relations and pagination conventions. It is REST-style with a chosen format and a community of tooling. A JSON:API response body always has a top-level data field (the primary resource or array of resources), an optional included field (the sideloaded related resources the server chose to embed to spare the client a round-trip), and a links object with pagination and self-references. The discipline is strong enough that a JSON:API client library (Ember Data is the original) can interact with any JSON:API server without per-API code.
HAL (Hypertext Application Language) is a much lighter convention — just _links and _embedded on top of arbitrary JSON. Spring HATEOAS speaks HAL natively. The minimal HAL requirement is that every resource representation includes a _links.self.href pointing to its own canonical URI, and that any further hypermedia controls go in _links keyed by IANA-registered or custom link relations (next, prev, author, edit). HAL is what most “level-3 in production” APIs actually ship.
OpenAPI / Swagger is an interface description language for level-2 REST: it lists endpoints, request/response schemas, and status codes. Fielding-pure REST is anti-OpenAPI in spirit (the server should describe itself via hypermedia, not via an out-of-band Interface Description Language file), but in practice OpenAPI is the dominant API-documentation and code-generation tool. Tools like swagger-codegen and openapi-generator produce client SDKs in dozens of languages from one OpenAPI document; the same document drives interactive documentation (Swagger UI, Redoc), request validators in API gateways, and contract tests. OpenAPI coexists peacefully with REST in industry usage and is now the de facto contract document.
HTTP API as a label. Some teams now distinguish “HTTP API” (level 2) from “REST API” (level 3) deliberately, conceding the ground rather than fighting Fielding’s vocabulary battle. Microsoft’s REST API Guidelines call their style “RESTful” while documenting that they intentionally do not require HATEOAS; AWS’s service APIs are similarly level-2 and do not pretend otherwise. The pragmatic position has effectively won.
gRPC-Gateway and dual-stack APIs. A common production pattern, especially in Go ecosystems, is to define services in protobuf for gRPC and also expose them as REST/JSON via a transcoding gateway (grpc-gateway). The proto annotations declare REST mappings (option (google.api.http) = { get: "/v1/users/{id}" }); the generator emits a reverse-proxy that translates incoming REST requests to gRPC calls. The team writes the service once, third parties consume it as REST, internal callers use gRPC. This is the architecture Google Cloud’s own APIs use — every public REST endpoint is backed by a gRPC implementation underneath.
7. Real-World Examples
Twitter / X API v2 — URL-versioned (/2/tweets/{id}), JSON, level 2, cursor-paginated. Demonstrates expansions (?expansions=author_id) as a pragmatic answer to under-fetching without going to GraphQL. The expansions design — naming related resources to inline — is a frequent middle-ground choice for REST APIs that want to mitigate under-fetching without committing to a query language.
GitHub REST API v3 — Path-versioned, custom media types for content negotiation (application/vnd.github.v3.raw+json), partial HATEOAS (some endpoints embed url fields pointing to related resources). The classic textbook case study. GitHub publishes both REST v3 and GraphQL v4 in parallel and documents which scenarios each is preferred for — REST for actions and webhook payloads, GraphQL for composing multi-resource reads.
Stripe API — Considered the platinum-standard commercial REST API. Date-based versioning with Stripe-Version header; Idempotency-Key for safe POST retries (a pattern they popularized industrywide); cursor pagination; rich error objects with stable error codes; expandable nested resources (?expand[]=customer). Worth studying line-by-line. Stripe’s secret is internal versioning machinery: every API change is dated; the server holds a translation pipeline that maps any request from any historical date to current internal models and back, so customers can pin to a 2018 version and still call new endpoints. This is the operational discipline that lets a public API live for a decade.
Kubernetes API — Surprisingly REST-shaped: every object (Pod, Service, Deployment) is a resource at a stable URI, manipulated by GET/POST/PUT/PATCH/DELETE, with watch as the streaming variant. Demonstrates that REST scales to control planes, not just user-facing APIs. Kubernetes also ships an OpenAPI-style schema (Custom Resource Definitions describe their schema) and uses the schema for server-side validation, kubectl autocompletion, and code generation — a complete REST-plus-IDL ecosystem.
PayPal / HAL-formatted APIs — One of the most-cited examples of level 3 in production, with HAL-encoded responses driving payment-flow state machines. A PayPal payment response includes _links for the next legitimate transitions (approve, capture, void) which the client follows rather than constructing.
AWS Service APIs — Each AWS service exposes a level-2-ish REST API (or a query-style API for older services) backed by enormous tooling. The pattern of Action=...&Param1=...&Param2=... query parameters in the older AWS APIs is a level-1 design that predates the REST consensus and survives for compatibility.
8. Tradeoffs and Comparison
8.1 REST vs. gRPC
gRPC is a Google-originated Remote Procedure Call (RPC) framework using HTTP/2 transport with Protocol Buffers (protobuf) for binary serialization. The contrast:
- Wire format: REST is human-readable JSON; gRPC is binary protobuf, 3–10× smaller and faster to parse.
- Contract: REST has no required IDL; gRPC requires a
.protofile from which client and server stubs are generated. - Transport: REST runs over HTTP/1.1 or HTTP/2; gRPC requires HTTP/2 and uses its multiplexing for streaming.
- Streaming: REST has no streaming primitive (Server-Sent Events or WebSocket Protocol are bolt-ons); gRPC supports unary, server-streaming, client-streaming, and bidirectional-streaming natively.
- Browser support: REST works in any browser; gRPC requires the gRPC-Web shim.
- Debugging: REST debugs with
curl; gRPC needsgrpcurland the.protoschema. - Use case fit: REST wins for public APIs (third-party integration ease) and human-debuggable internal APIs; gRPC wins for high-throughput service-to-service traffic where binary efficiency and streaming matter.
8.2 REST vs. GraphQL
GraphQL is a Facebook-originated query language where the client specifies exactly which fields it wants. The contrast:
- Endpoint count: REST exposes many endpoints; GraphQL exposes one (
POST /graphql). - Over- and under-fetching: REST returns server-defined views, leading to over-fetching (more fields than needed) and under-fetching (multiple round-trips for nested data); GraphQL eliminates both by letting the client specify the shape.
- HTTP-level caching: trivial in REST (cache
GETby URL); difficult in GraphQL (every query is a uniquePOST, requires application-level cache keyed by query+variables). - Tooling: both are mature; GraphQL has Apollo/Relay; REST has the entire HTTP ecosystem.
- Learning curve: REST is conceptually lighter; GraphQL has its own type system, resolver pattern, and pitfalls (the N+1 problem).
8.3 What REST Is Good At
- Public APIs where third-party developer ergonomics dominate.
- Long-lived, slowly-evolving interfaces — the URI structure can be stable for a decade.
- CRUD-shaped domains (resources that genuinely look like records).
- Caching-heavy workloads — HTTP cache infrastructure is unmatched.
8.4 What REST Is Bad At
- Highly nested or graph-shaped data with many client-driven views — leads to ad-hoc
?expand[]parameters that drift toward GraphQL anyway. - Real-time push (SSE and WebSocket are bolt-on, not native).
- High-throughput service-to-service RPC where binary-protobuf-over-HTTP/2 (gRPC) dominates.
- Non-CRUD domains where actions don’t map to verbs cleanly (
POST /orders/77/refundis neither create nor replace; it’s an action) — the URI starts to resemble RPC anyway.
9. Pitfalls and Common Mistakes
Calling level-2 designs REST and expecting Fielding-pure properties. A level-2 API does not give you the evolvability HATEOAS would; clients still hardcode URLs. This is fine, but be honest about it.
CRUD-mapping a non-CRUD domain. “Refund this charge,” “fire this employee,” “publish this draft” do not map cleanly onto POST/PUT/PATCH/DELETE. Common workarounds: action sub-resources (POST /charges/77/refund), state-property updates (PATCH /drafts/77 {"status":"published"}), or a parallel “actions” resource. None feel native; choose the least-bad one and document it.
Over-fetching. A GET /users/42 that returns 80 fields when the client wants 3 wastes bandwidth, especially on mobile. Mitigations: sparse fieldsets (?fields=id,name), expansion parameters, separate “summary” and “full” endpoints — each adds API surface. This is the canonical complaint that motivated GraphQL.
Under-fetching / N+1 round trips. A client showing a feed of 20 posts with author names does GET /posts then 20 × GET /authors/{id}. Mitigations: server-side embedding (?expand[]=author), denormalized representation, or a dedicated “feed” endpoint. Each violates orthogonality: now there’s a posts endpoint and a posts-with-authors endpoint, both of which must be maintained.
Treating POST as a generic action verb. POST /sendEmail, POST /generateReport are level-1 RPC dressed in REST clothing. Sometimes legitimately unavoidable (truly action-shaped operations); sometimes a missed resource-modeling opportunity (POST /reports to create a report job is RESTful — the report itself is the resource).
Returning 200 OK for failures. A server responding 200 OK {"error": "not found"} defeats every generic intermediary that uses the status code to decide whether to retry, alert, log as error, or cache. Use the status codes.
Plural/singular inconsistency. /users/42/order/77 and /users/42/posts/77 in the same API is a code-review smell. Pick plural everywhere and stick to it.
Missing idempotency keys on POST. A network blip during checkout that makes the client retry can charge a customer twice. Stripe’s Idempotency-Key pattern is now the industry expectation: client generates a UUID, server ensures retry with the same key returns the original result.
Over-eager versioning. Bumping the version for additive changes (a new optional field) when the existing version could have absorbed it forks the API surface needlessly. The Postel-style rule — be conservative in what you send, liberal in what you accept — argues for version bumps only on backward-incompatible changes.
Ignoring cache headers. A GET endpoint that returns Cache-Control: no-store for everything pays full origin cost on every request. Audit cache directives during design, not after a load incident. The opposite mistake — caching too aggressively — bites when an endpoint that should be private is cached by an intermediary and a different user receives the cached response. Use Cache-Control: private for per-user data, public only when the response is genuinely the same for all callers, and Vary to declare which request headers (e.g., Accept, Authorization) participate in the cache key.
Trailing slashes and case sensitivity. /users/42 and /users/42/ are technically distinct URIs per RFC 3986. Decide on a convention (most APIs settle on no-trailing-slash) and either redirect or 404 the alternative. The same applies to case — most APIs are case-sensitive in path components and case-insensitive in query parameter names; document and enforce.
Confusing 401 and 403. 401 Unauthorized (a misnomer; means unauthenticated — credentials missing or invalid) versus 403 Forbidden (authenticated but the action is not permitted). Conflating them confuses clients about whether to re-authenticate or to give up.
10. Interview Discussion
In a senior-level system-design interview, REST appears as the assumed baseline rather than a topic on its own. Expect:
- “Why did you choose REST over gRPC for the public API?” Answer in terms of third-party developer ergonomics, browser support, debuggability, the absence of streaming requirements, and cache infrastructure.
- “How do you version this API?” Have a position on URL vs. media-type vs. date-based versioning, and explain when you’d pick which.
- “Walk me through the lifecycle of a request from client to server.” Expect to cover Domain Name System (DNS) resolution, Transport Layer Security (TLS) handshake, API gateway auth, routing, the upstream service, the response path, status-code semantics, and caching.
- “How do you handle pagination?” Pivot from offset/limit (acknowledge), to cursor-based (recommend at scale), to the
Linkheader convention or hypermedia links. - “How do you make
POSTretry-safe?” Idempotency keys, with the storage and TTL design behind them. - “When would you reach for GraphQL or gRPC instead?” The §8 answers above.
- A trap question: “Is this REST?” When the design has no HATEOAS — the technically-correct answer is “by Fielding’s definition, no — it’s level 2 in Richardson’s model — but in industry usage everyone calls this REST.” Showing you know the distinction without being pedantic about it scores.
The depth signal is whether you can articulate REST as trade-offs against gRPC and GraphQL rather than reciting it as a list of rules. The constraints exist because each unlocked a property — describe what each one buys.
11. See Also
- gRPC — high-performance RPC alternative, contrasts with REST on every dimension
- GraphQL — single-endpoint client-shaped query alternative
- Client-Server Architecture — the foundational style REST refines
- API Gateway System Design — the gateway tier most REST APIs sit behind
- Backend for Frontend Pattern — the BFF variant that arises when one REST API is too monolithic for many client classes
- Microservices Architecture — the architecture inside which most REST APIs live
- Service Mesh System Design — the layer-7 layer beneath the REST contract
- WebSocket Protocol — the streaming bolt-on that REST does not natively provide
- Server-Sent Events — the simpler push primitive REST APIs sometimes use
- Authorization Code Flow — the OAuth 2.0 flow that REST APIs commonly authenticate with
- SWE Interview Preparation MOC
- Major System Designs MOC