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.
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.
Test Your Understanding
Quick architectural scenario challenge based on this breakdown.
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 –
Sign in to share your thoughts
Nothing yet. Be the first.