OwlTheEngineer
How It Actually WorksEpisode 16 · Backend

What REST actually means, and the two constraints that bite

REST is six constraints in one chapter of one dissertation, not a synonym for JSON over HTTP — and the spec's own example of getting it wrong is a link a crawler follows to delete your rows.

Owl · 9 min read · 0 views
– comments
Ep 16 · Backend
60s VISUAL BREAKDOWN
Episode 16
Watch the fast visual breakdown or discuss on social:

A crawler followed a link and the rows are gone. Nothing malicious happened, nobody was logged in as an admin, and the link it followed was one your own admin page put on the screen: page?do=delete. That string is not a hypothetical. It is the example RFC 9110 itself reaches for when it explains what a safe method is, and the spec puts the blame somewhere most people don’t expect.

This is the written half of a ninety-second video, and the video had room for two of REST’s six constraints. What follows is the other material — the method that is neither safe nor idempotent, the header that makes a retry survivable and isn’t a standard, the interface constraint nobody ships, and the reason the popular way of teaching all this is absent here on purpose.

The two the video had room for

REST is an architectural style, described in chapter 5 of Roy Fielding’s 2000 dissertation, derived by adding constraints to a system that has none. HTTP is a protocol. They are not the same thing, and the tell is that the same person wrote both sides: Fielding wrote chapter 5, and he is the first-named editor of RFC 9110, twenty-two years later.

The first constraint that bites is identification of resources — an address names a noun, and the method says what you are doing to it. RFC 9110 §3.1 is deliberately broad about what a noun can be: “The target of an HTTP request is called a ‘resource’. HTTP does not limit the nature of a resource; it merely defines an interface that might be used to interact with resources.” POST /getUserList takes that interface and throws it away, because the verb is now in the name and the method is decoration.

The second is statelessness, and it is the one that gets paraphrased into meaninglessness. It does not mean the server keeps no state. Fielding §5.1.3: communication “must be stateless in nature … such that each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server,” and then, immediately: “Session state is therefore kept entirely on the client.” Keep all the data you like. What you may not keep is this client’s session context between requests — which is why an in-memory session on server A logs the user out the moment the load balancer sends the next request to server B.

The spine of the video was the safe/unsafe distinction, so the short version: GET, HEAD, OPTIONS and TRACE are safe (§9.2.1), safe means “the client does not request, and does not expect, any state change,” and the purpose of the whole distinction is “to allow automated retrieval processes (spiders) and cache performance optimization (pre-fetching) to work without fear of causing harm.” Put an action behind a GET and you have volunteered your rows to every crawler that walks the page. The MUST in that section is aimed at you, not at the crawler: “it is the resource owner’s responsibility to ensure that the action is consistent with the request method semantics.”

POST is not idempotent, and the fix is not in any RFC

Safe and idempotent are different properties, and collapsing them is where retries go wrong. Safe is no change was asked for. Idempotent is asking twice lands the same as asking once. DELETE is idempotent and emphatically not safe. RFC 9110 §9.2.2: “Of the request methods defined by this specification, PUT, DELETE, and safe request methods are idempotent.” POST is conspicuously not on that list.

That absence is why idempotency is worth a section. §9.2.2 explains what the property is actually for: “Idempotent methods are distinguished because the request can be repeated automatically if a communication failure occurs before the client is able to read the server’s response.” A dropped connection after a PUT tells you nothing about whether the server acted — and with PUT you don’t need to know, because you can just send it again. After a POST you do need to know, and there is nothing in the method to help you. The spec draws the line explicitly: “A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent.”

Read the escape clause in that sentence, because the whole practitioner’s fix lives in it: unless it has some means to know. You supply the means. The client generates a unique key, sends it with the request, and the server stores the outcome against that key so a second arrival returns the first result instead of charging the card again. Stripe’s implementation is the one everyone copied, and it is worth quoting for the detail people skip: “Stripe’s idempotency works by saving the resulting status code and body of the first request made for any given idempotency key, regardless of whether it succeeds or fails. Subsequent requests with the same key return the same result, including 500 errors.” Keys are pruned “after they’re at least 24 hours old,” and — the part that catches teams out — “The idempotency layer compares incoming parameters to those of the original request and errors if they’re not the same.” A retry that quietly changed the amount is not a retry.

Now the honest part, which most explainers get wrong. Idempotency-Key is not a standard. It was an IETF Internet-Draft, draft-ietf-httpapi-idempotency-key-header, it reached revision 07, and as of 2026-09-20 the datatracker lists it as expired and archived, intended RFC status: none. It is a convention that Stripe popularised and the rest of the payments industry copied, and it is a good one — but if you write “per the spec” in a code comment next to it, the comment is wrong. Check the document’s state on the day you cite it; that is precisely the class of fact that rots.

PATCH, the method that is neither

PATCH is not in RFC 9110’s method set at all. It is defined by RFC 5789, PATCH Method for HTTP, March 2010, Standards Track, and its section 2 says the quiet part plainly: “PATCH is neither safe nor idempotent as defined by [RFC2616], Section 9.1.”

Two things follow. First, everything in the section above applies to PATCH as much as to POST — do not let a client library retry it for you on a timeout. Second, look at what that sentence cites. RFC 2616 was obsoleted in 2014, by the RFC 723x series, which was itself obsoleted in 2022 by RFC 9110. The definition PATCH points at has moved twice since PATCH was written. The meaning survived the moves intact, which is the system working; the citation is a small reminder that a section number is a fact with a shelf life.

The practical confusion is that a particular PATCH can be idempotent in effect — {"status": "shipped"} sets an absolute value and lands the same however many times it arrives, while {"quantity_delta": 1} does not. That is a property of your payload, not of the method, and no intermediary can tell the difference. The method is what caches, proxies and retry logic reason about, so the method’s answer is the one that governs.

Self-descriptive messages, and the constraint nobody ships

The uniform interface is “the central feature that distinguishes the REST architectural style from other network-based styles” (§5.1.5), and it is defined by four constraints, not one: “identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state.”

Self-descriptive is the third, and it is the one doing invisible work every day. A message carries what is needed to interpret it — Content-Type saying what the bytes are, Accept saying what the client will take, Cache-Control saying what may be stored. This is what makes the layered system constraint (§5.1.6) possible, where “each component cannot ‘see’ beyond the immediate layer with which they are interacting.” A CDN that has never heard of your application can still cache your response correctly, because the response explains itself. An API that encodes meaning in an undocumented integer in the body has quietly opted out.

Hypermedia as the engine of application state is the fourth, and it is the one almost nothing implements. The idea: a response carries not just data but the available next moves, as links, so the client discovers what it can do rather than hard-coding it.

{
  "id": 42,
  "name": "Wireless Mouse",
  "price": 29.99,
  "stock": 17,
  "_links": {
    "self":     { "href": "/products/42" },
    "reviews":  { "href": "/products/42/reviews" },
    "add-to-cart": { "href": "/carts/current/items", "method": "POST" }
  }
}

The payoff is that the server can move /products/42/reviews to another host and no client breaks, because no client ever spelled that address. Fielding is famously unwilling to soften this. From his 2008 post, REST APIs must be hypertext-driven: “if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.” And, in the same post: “A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server).”

By that standard essentially every API called REST is not one, including good ones. It is worth being clear about why, without either pretending the constraint doesn’t exist or pretending everyone is a fool for ignoring it. Hypermedia buys evolvability — the freedom to change addresses without a client release. Most teams ship the client and the server together, from one repository, on one deploy, and a coupling you can refactor in an afternoon is not a coupling worth an extra protocol layer to remove. The constraint is aimed at the case where you cannot reach your clients: the open web, long-lived integrations, third parties you have never met. When that is your situation, HATEOAS stops looking academic.

Fielding priced the whole style honestly in the same chapter: “The trade-off, though, is that a uniform interface degrades efficiency, since information is transferred in a standardized form rather than one which is specific to an application’s needs,” and REST “is designed to be efficient for large-grain hypermedia data transfer … resulting in an interface that is not optimal for other forms of architectural interaction.” That last clause is not a loophole. It is the author saying his style has a domain, and that outside it something else may fit better — which is the entire subject of the next episode.

What this post is deliberately not

It is not a maturity model. The usual way to teach this material is a ladder of levels, ending at hypermedia, and it is absent here on purpose: it is a secondary framing of a primary source that is already clear. The constraints are in chapter 5, the method semantics are in RFC 9110, both are free, and reading either beats reading a summary of it — a lesson this episode learned the hard way when a summariser confidently produced a requirement about retrying safe requests that appears nowhere in the RFC.

It is also not about status codes, which are the next episode’s whole subject, or about caching, which is a named REST constraint (§5.1.4) and gets two episodes of its own. And it is not an argument that your API “isn’t really REST.” The useful question was never whether the label fits. It is whether a crawler can delete your rows.

SYSTEM DESIGN CHECK

Test Your Understanding

Quick architectural scenario challenge based on this breakdown.

SCENARIO

A 3rd-party payment gateway starts timing out with 504 errors during peak traffic.

Q1.How should a downstream dependency failure be handled to protect upstream service availability?

Comments –