Summary (TL;DR)
Roy Fielding's 2000 dissertation defined REST with six constraints: Client-Server, Stateless, Cacheable, Uniform Interface (with four sub-constraints including HATEOAS), Layered System, and Code on Demand. Most APIs only meet two or three, often skipping caching and HATEOAS. The article explains each constraint, shows how skipping HATEOAS leads to problems like inconsistent pagination and versioning, and argues that while internal APIs can skip it, public APIs benefit from the full set. It also gives Siren hypermedia examples for HATEOAS.
Roy Fielding derived six constraints in 2000. Most APIs meet two or three of them.

Many devs have built a REST API in their careers. Few have read the dissertation that defines it. The gap between the popular understanding and the original definition is the source of most architectural confusion and API instability in the past couple of decades.
Roy Fielding published his doctoral thesis at UC Irvine in 2000. Chapter 5 derives six constraints for the architecture of distributed hypermedia systems. He starts from the “null style,” an empty set of constraints, and adds each one incrementally, analyzing the properties it induces. The result is an architectural style he named “Representational State Transfer.”
The word “style” is Fielding’s term for a coordinated set of constraints. REST is one style. Client-server, stateless, cacheable, and the rest are the constraints that compose it. The term sounds subjective, but the constraints themselves are not.
The industry adopted the name and ignored most of the constraints. “REST” now means “any API that sends JSON over HTTP.” That definition is useful for a conversation between two engineers that do not know each other, but it is not useful to discuss architecture. I have reviewed enough API designs to see the same mistakes repeat. Teams skip constraints they did not know existed and then spend time correcting their own mistakes.
Constraint 1: Client-Server (mandatory)
The client and the server have separate concerns. The client handles the interface. The server handles data and logic. Most APIs get this one right by default. The violation shows up when the server dictates how the client should render things.
Say the API returns displayOrder: 3 and buttonColor: "#ff0000" for an action. The display order should come from the position of elements in the response. The color should come from a property such as class: ["danger"] on the action in a way that it has meaning. A button in a UI or a prompt option in a CLI can interpret those on its own terms.
Constraint 2: Stateless (mandatory)
Every request contains all the information the server needs to process it. The server stores no session state between calls.
Say you send GET /path-1 with a session cookie. The server looks up that cookie in memory to find your user ID. That lookup is server-side state. A stateless version puts the user ID in the request itself. A JWT or a POST body can carry it along and return it again from the response body so clients can re-send.
Constraint 3: Cacheable (mandatory)
Responses must be labeled, implicitly or explicitly, as cacheable or non-cacheable. The client or an intermediary can then reuse cached responses without hitting the server again. Fielding treated this as a first-class architectural concern that improves efficiency, scalability, and user-perceived performance by reducing average interaction latency. It was not an optimization to bolt on later.
Most JSON APIs ignore caching entirely. You send GET /articles/42 and the response has no Cache-Control header, no ETag, no Last-Modified. Your client hits the server every time, even when the article has not changed in weeks.
Constraint 4: Uniform Interface
This is the big one. Fielding broke it into four sub-constraints.
Constraint 4.1: URIs
URIs identify resources. The uniformity here is the URI spec itself: scheme, authority, path, query, fragment. Every resource on the web uses the same identifier format. That is what the constraint requires. It says nothing about how you structure the path segment.
/articles/42, /x?id=42, and /a/b/c are all valid URI paths. Most developers confuse this constraint with "use clean URL paths". That is a popular convention and a great SEO tool, not what Fielding is referring to.
Constraint 4.2: Manipulation of the "R" of "URI"
The second sub-constraint is manipulation of resources (an abstract concept which is identified via the Uniform Resource Identifier) through representations.
You perform a GET into /whatever to retrieve a representation of that resource with standard cacheable semantics.
You perform a PUT into /something with a request body. The Content-Type header on the request tells the server the format of the body you are sending. The Accept header tells the hypermedia types the client supports for the response.
The resource itself is an abstract concept. It is not the JSON, the HTML, or the bytes on the wire. A browser that accepts text/html gets a web page. An API client that accepts application/vnd.siren+json gets Siren. Same URI, same resource, different representation depending on what the client accepts.
Constraint 4.3: Messages describe themselves
A response with Content-Type: application/vnd.collection+json tells the client how to parse the body without guessing.
See 4.4 HATEOAS for examples.
Constraint 4.4: HATEOAS
Get Fayner Brack’s stories in your inbox
Join Medium for free to get updates from this writer.
The fourth sub-constraint is Hypermedia As The Engine Of Application state, or HATEOAS. Most APIs handle the first three. HATEOAS is where almost every API stops.
The difference shows up clearly in a reading list API. Without HATEOAS, you get plain data like a database record:
{
"id": 42,
"title": "How Browsers Work",
"url": "https://example.com/browsers",
"status": "unread"
}The client knows nothing about what it can do next. Marking the article as read requires the client to already know the endpoint: PATCH /articles/42 with {"status": "read"}. The developer hardcoded that knowledge from reading documentation. The API itself did not communicate it.
With HATEOAS, the server tells the client what actions are available in a standard manner. Here is the same response using Siren, a hypermedia type:
{
"class": ["article"],
"properties": {
"id": 42,
"title": "How Browsers Work",
"url": "https://example.com/browsers",
"status": "unread"
},
"actions": [
{
"name": "mark-as-read",
"href": "/articles/42",
"method": "PATCH",
"fields": [
{ "name": "status", "value": "read" }
]
},
{
"name": "delete",
"href": "/articles/42",
"method": "DELETE"
}
],
"links": [
{ "rel": ["self"], "href": "/articles/42" },
{ "rel": ["collection"], "href": "/articles" },
{ "rel": ["next"], "href": "/articles/43" }
]
}The client does not hardcode URLs or HTTP methods. The actions array tells it what it can do. Navigation comes from links. A generic HTML client can loop through the actions and render controls for each one:
<div id="article-actions"></div>
<script>
const response = await fetch('/articles/42' /* bookmarked entry-point */, {
headers: { 'Accept': 'application/vnd.siren+json' }
});
const entity = await response.json();
const container = document.getElementById('article-actions');
for (const action of entity.actions) {
const button = document.createElement('button');
button.textContent = action.name;
button.onclick = () => fetch(action.href, { method: action.method });
container.appendChild(button);
}
</script>This client mentions no specific endpoints, except for the entry-point. The server adds a new action. Every client picks it up on the next request, with no deployment needed. Notice the Accept: application/vnd.siren+json header in the fetch call, the JavaScript code (client) requests the Siren media type on the same URL the browser uses.
The server does not need a separate /api route. It checks the Accept header and returns HTML for browsers, Siren for API clients. Same resource, different representation. That is content negotiation. It is part of the uniform interface constraint.
If the article is already read, the server omits the mark-as-read action from the response. The client's UI removes the button. No if statement in the client code. The server controls the available transitions.
Constraint 5: Layered System (mandatory)
The client cannot tell whether it talks to the end server or to an intermediary. Load balancers, CDNs, and API gateways should be invisible to the caller. Most APIs satisfy this one without trying. The violation happens when error messages expose the hostname of a backend service. That breaks the layer boundary.
Constraint 6: Code on Demand (optional)
The server can send executable code to the client. Think JavaScript served by a web page via the <script> tag. For APIs, this one barely applies. Fielding made it the only optional constraint.
That is the checklist: six constraints. Most APIs satisfy two or three of them: client-server, layered system, and partial statelessness. Most violate cacheability by omission. Most ignore HATEOAS entirely.
Does the gap matter? For most teams building internal services, no. Ten consumers and a Slack channel are enough to coordinate changes by talking to people. The label “REST API” works as shorthand for “HTTP plus JSON with resource-oriented URLs.”
A public API is different. So is an API consumed by teams outside your organization or by an untrusted boundary within. In that scenario, the skipped constraints start to cost you. The most expensive one to skip is HATEOAS.
Fielding derived HATEOAS to address what API teams now struggle to solve by hand, repeatedly, and differently each time.
Take pagination. Without HATEOAS, every API invents its own scheme. One uses page and pageSize. Another uses offset and limit. A third uses cursor-based tokens. Another one uses GraphQL, so every time the API changes the clients crash and are forced to adjust abruptly.
With HATEOAS, the server includes a link with a relation that means “the next page.” The client follows that relation with a link constructed in the server that can return a ?page=2 and increment when a "next" is requested. It does not care whether the format calls it next, rel: ["next"], or something else. The pagination scheme can change without breaking the client. The client had no knowledge of the scheme and the URL format. It only knew the relation and how to communicate back to the server.
Versioning is the same story. Without HATEOAS, teams version through URL paths (/v1/, /v2/) or custom headers X-API-Version. Taking months to strangle older versions and maintaining multiple code paths or internal redirect interfaces for years. Clients pin to a version and break at retirement.
With HATEOAS, the server introduces new actions by adding links. Old links keep working. The client adapts by following links rather than memorizing paths.
Discoverability is the third cost. Without HATEOAS, a developer’s first step is reading Swagger docs. Her second step is hardcoding every endpoint path into the client. With HATEOAS, the API root returns links to every available resource. The client explores the API the way a browser explores a website. To avoid doing all those hops again later, offline caching headers are part of the design.
Fielding designed REST to describe the web, not APIs. The dissertation was written alongside his co-authorship of the HTTP/1.1 and URI specifications. REST is a distillation of the principles that guided those standards. Applying it to JSON APIs came later, and that later application is where most of the constraints got dropped.
Browsers do not hardcode URLs. They follow links. A browser reads a form to know what to submit. The server’s response tells it what to do next.
A browser is a desktop app as much as a React application is a web app.
Popular “REST APIs” dropped both of those patterns. HATEOAS adds real engineering expertise required to design response payloads. It requires a media type that supports links, like Siren, HAL, or JSON:API. The client side has its own cost: processing link-driven responses instead of hardcoded URLs. For a small team with a single consumer and lack of expertise, that cost is not justified.
But the cost of skipping it grows with every new consumer, version migration, and breaking change. Small APIs can afford to skip HATEOAS, but large APIs cannot afford to keep reinventing the wheel and try to rebuild what some pre-arranged HATEOAS formats already handle.
Run your API through the six constraints. Count how many you satisfy. That count is not a score. It is a map of the trade-offs you made, on purpose or by accident. The exercise will answer this one question:
Is your API a Representational State Transfer (REST) API or just a hacked HTTP message format that is closed for extension?
If you liked this, you might like readplace.com, built for exactly this kind of reading.
Thanks for reading. If you have some feedback, reach out to me on LinkedIn, Reddit or by replying to this post.