C.W.K.
Stream
Lesson 04 of 07 · published

HATEOAS — The Honest Reckoning (What Ships in 2026)

~11 min · rest-design, hateoas, honest, rpc-over-http

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Fielding said your API isn't RESTful unless it's hypertext-driven. The industry shrugged and shipped HTTP+JSON APIs anyway, calling them REST. Both stances are defensible. Pretending the gap doesn't exist isn't."

The Empirical Reality of 'REST' in 2026

Survey the most-integrated APIs in production:

  • Stripe — resource-oriented URIs, no HATEOAS. Clients use generated SDKs that hardcode URI patterns.
  • OpenAI — RPC-shaped (/v1/chat/completions). No HATEOAS pretense.
  • GitHub REST API — has some HATEOAS holdovers (URLs in responses, pagination Link headers), but most clients ignore them and hardcode URIs anyway.
  • AWS — heavily RPC, signed requests, no hypermedia.
  • Twilio, SendGrid, Slack, Anthropic, Google APIs — none are HATEOAS-driven.

What virtually all of them are, by Fielding's strict definition, is "RPC over HTTP with URI conventions and JSON bodies." Not REST in the dissertation sense. And they all work, scale, and ship.

Why HATEOAS Didn't Win

The benefits HATEOAS offers (URI evolution, dynamic capability discovery, generic clients) require costs that most API consumers couldn't or wouldn't pay:

1. Clients aren't generic. The dream of a single "HATEOAS client" walking any JSON API the way a browser walks any HTML site never materialized. Every API has business semantics that can't be expressed in links alone — error handling, retry policy, edge cases. A generic client that follows links but doesn't know what an order is can't actually do anything useful. So clients become API-specific, and the moment they're API-specific, they hardcode URI templates because that's faster than walking links every request.

2. Tooling assumes URI templates. OpenAPI, JSON Schema, generated SDKs, request/response validators — all are built around the assumption that the API has stable URI templates the client knows. Adopting HATEOAS means giving up most of this tooling, or building it twice.

3. Heavier responses. Every resource carries a links block that may double its size. For high-throughput APIs this is real bandwidth.

4. Testing is harder. You can't just "call /users/42" — you have to navigate from the root, follow links, find the user. Testing tools have to simulate the walk.

5. The "URI evolution" benefit is theoretical. Production APIs rarely change URIs in practice; they version (Track 2 lesson 5) instead. The escape hatch HATEOAS provides is for a problem most teams don't have.

What Most APIs Actually Ship — The Honest Middle

Real-world APIs adopt some hypermedia hints without committing to full HATEOAS:

  • Location header on 201 Created. The most universal hypermedia hint. New resource → here's its URI.
  • Pagination Link headers (RFC 8288). Link: </items?cursor=abc>; rel="next". The client follows the next-page link instead of computing the URI. GitHub does this.
  • Redirects with Location. 301/302/307/308 all carry the next URI in Location. Hypermedia, lightweight.
  • Sub-resource URIs in responses. A user resource may include {"orders_url": "..."} as a convenience without making the client follow it.

This is the "REST API" most teams ship: resource-oriented URIs, the seven methods used semantically, status codes that actually mean something, plus a handful of hypermedia hints where they pay for themselves. Not pure REST. Not pure RPC. Coherent and pragmatic.

HATEOAS is a tool, not a virtue. Use it where the cost pays for itself (server-driven UIs, dynamic workflows, admin tools with complex permissions). Skip it where it doesn't (typical CRUD APIs, integration-focused services). Either choice is defensible. Calling something REST without HATEOAS, then defending the choice in those terms, is also defensible — Fielding's right that it's not pure REST; you're right that the trade-off worked.

When HATEOAS Genuinely Wins

  • Server-driven UIs. Frameworks like Spring HATEOAS, Hotwire/Turbo (HTML-based), or hypermedia-driven mobile apps use the server-driven-link approach to ship UI changes without redeploying clients.
  • Workflow engines. When the available next-steps genuinely depend on resource state + user permissions + business rules, expressing them as links from the server is simpler than encoding the state machine on every client.
  • Admin tools with dynamic permissions. The buttons a user sees should match their authorization. Server includes/omits links based on auth, client just renders.
  • Long-lived, evolving APIs with diverse clients. If you're Twilio or Stripe with thousands of clients you can't recompile, HATEOAS gives you URI freedom — but you'd ALSO need to ship a generic client, which is its own project.

The Honest Framing

Stop arguing about whether your API "is REST." Frame the design choices instead:

  • "This is a resource-oriented JSON-over-HTTP API. URIs name nouns. We use the seven HTTP methods semantically. Status codes communicate outcomes. Clients hardcode URI templates and use a generated SDK."
  • "This is a workflow API with HATEOAS-style links. The client discovers available transitions from the server. We use HAL-formatted responses."
  • "This is an RPC-over-HTTP API. URIs name operations; everything is POST; we return structured error objects."

Any of these is honest. "REST API" without elaboration is marketing.

cwkPippa's Honest Label

cwkPippa's API is "resource-oriented JSON-over-HTTP with a handful of action endpoints and zero HATEOAS." The frontend hardcodes every URI it calls. The Location header on 201 is the only hypermedia hint. This is the right shape for cwkPippa — single client, deployed in lockstep, two-user audience. If we ever ship a public API to third parties, the calculation might flip toward more hypermedia hints (pagination links, sub-resource URLs in responses), but probably never toward pure HATEOAS. The honest label saves everyone a debate.

Code

Hypermedia hints — partial REST that ships in real APIs·http
# The honest middle — hypermedia hints without full HATEOAS

# 1. Location header on 201 Created — universal hypermedia hint
HTTP/1.1 201 Created
Location: /orders/o_xyz
Content-Type: application/json

{"id":"o_xyz","status":"pending"}

# 2. Pagination Link header (RFC 8288) — follow next/prev without computing URIs
HTTP/1.1 200 OK
Link: </items?cursor=abc>; rel="next", </items?cursor=zyx>; rel="prev"
Content-Type: application/json

{"items": [...], "cursor": "abc"}

# 3. Convenience sub-resource URLs (not enforced HATEOAS, just helpful)
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "u_42",
  "name": "Pippa",
  "orders_url": "https://api.example.com/users/u_42/orders"
}
Pragmatic client — hardcoded URIs + Location + Link header awareness·python
# A pragmatic client — hardcoded URIs + respect for Location and Link headers
import httpx

class PragmaticClient:
    """Real-world client: hardcoded URI templates + opportunistic hypermedia."""
    BASE = 'https://api.example.com'

    def __init__(self, token: str):
        self.client = httpx.Client(
            base_url=self.BASE,
            headers={'Authorization': f'Bearer {token}'},
        )

    def create_order(self, payload: dict) -> dict:
        resp = self.client.post('/orders', json=payload)
        resp.raise_for_status()
        # Respect Location header — saves us computing the new URI
        new_url = resp.headers.get('Location')
        return {'id': resp.json()['id'], 'url': new_url}

    def list_orders_all_pages(self):
        """Follow pagination Link headers instead of computing pages ourselves."""
        url = '/orders'
        while url:
            resp = self.client.get(url)
            resp.raise_for_status()
            for item in resp.json()['items']:
                yield item
            # RFC 8288 Link parsing — follow next if present
            next_link = self._parse_link_header(resp.headers.get('Link', ''), 'next')
            url = next_link

    @staticmethod
    def _parse_link_header(header: str, rel: str) -> str | None:
        # Simplified parser; production: use a library like `linkheader`
        for part in header.split(','):
            if f'rel="{rel}"' in part:
                return part.split(';')[0].strip(' <>')
        return None
When HATEOAS genuinely wins — server-driven workflow UI·python
# When HATEOAS DOES win: a server-driven workflow client
from dataclasses import dataclass
import httpx

@dataclass
class WorkflowAction:
    name: str
    method: str
    href: str

class WorkflowClient:
    """This client KNOWS NOTHING about workflows except how to read links."""
    def __init__(self, entry: str):
        self.client = httpx.Client()
        self.entry = entry

    def available_actions(self, resource_url: str) -> list[WorkflowAction]:
        resp = self.client.get(resource_url).json()
        actions = []
        for rel, link in resp.get('_links', {}).items():
            if rel in ('self', 'parent'):
                continue
            actions.append(WorkflowAction(
                name=rel,
                method=link.get('method', 'GET'),
                href=link['href'],
            ))
        return actions

    def perform(self, action: WorkflowAction, payload: dict | None = None):
        return self.client.request(action.method, action.href, json=payload).json()

# UI renders a button per available_actions() entry — server controls the workflow.
# Adding a new state with new transitions on the server adds buttons here WITHOUT
# code changes. This is HATEOAS earning its keep.

External links

Exercise

Pick three APIs you've integrated with (Stripe, GitHub, OpenAI, Slack, your own — three of these or others). For each, audit: (1) Are URIs hardcoded by the SDK/client, or do clients walk links? (2) Does the server include hypermedia hints (Location, Link, _links)? (3) Could the API be fairly called 'REST' by Fielding's strict definition? Be honest. Then write a one-paragraph 'honest label' for one API you maintain or use heavily — describe what it actually is, not what it markets itself as.
Hint
Most APIs will fail the Fielding test. That's fine — the point is the audit, not a judgment. Stripe and OpenAI both hardcode URIs in their SDKs; both include Location on 201; both call themselves REST. GitHub includes pagination Link headers (genuine hypermedia hint) but most clients ignore them. The honest label for cwkPippa: 'resource-oriented JSON-over-HTTP with action endpoints and zero HATEOAS, designed for a single first-party React client.' Yours will look similar.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.