What is HATEOAS in REST APIs?
The most misunderstood constraint of REST, explained from first principles — why Roy Fielding invented it, why almost nobody implements it correctly, and how to actually build hypermedia‑driven APIs in production with Spring Boot.
Introduction & History
HATEOAS stands for Hypermedia As The Engine Of Application State. It is a constraint of the REST (Representational State Transfer) architectural style which says that a client should be able to interact with an API entirely through the hypermedia links provided dynamically by the server, rather than through hard‑coded knowledge of the API’s structure baked into the client beforehand.
In plain terms: instead of your API just returning raw data, it also returns the next possible actions as clickable links — the same way a web page returns links you can click, not a manual telling you which URLs to type into your browser.
Where did it come from?
REST was defined in the year 2000 by Roy Fielding in his PhD dissertation at UC Irvine, titled “Architectural Styles and the Design of Network‑based Software Architectures.” Fielding was one of the principal authors of the HTTP specification itself, and REST was his attempt to describe, after the fact, the architectural properties that made the World Wide Web scale so successfully.
HATEOAS is not an add‑on to REST — it is one of REST’s original constraints, described alongside statelessness, cacheability, a uniform interface, and a layered system. Fielding considered it essential enough that in 2008, frustrated by how the industry was using the word “REST” for APIs that were really just “HTTP + JSON with URLs,” he wrote a now‑famous blog post insisting that an API cannot be called RESTful unless it is driven by hypermedia.
Think of the World Wide Web itself. You don’t memorize every URL on Amazon.com. You land on the homepage, and the page tells you — via links — what you can do next: search, view a product, add to cart, checkout. HATEOAS asks: why don’t we build APIs the same way?
Why it matters today
Most APIs you have used — including the majority of “REST APIs” documented on Swagger/OpenAPI — are not truly RESTful in Fielding’s sense. They are what he’d call RPC over HTTP: a fixed, pre‑agreed set of endpoints that the client team hard‑codes based on documentation. HATEOAS is the one constraint almost nobody fully implements, which is exactly why it deserves a deep, honest explanation rather than a hand‑wave.
The web as the original working example
It helps enormously to remember that Fielding wasn’t inventing something abstract — he was reverse‑engineering the design decisions that already made the web itself so resilient. Consider what happens when you use a browser: you type one URL (say, a search engine’s homepage), and from that single starting point you can reach essentially the entire reachable web, purely by clicking links that the current page presents to you. Nobody hands you a master list of every URL on the internet ahead of time. Pages change, get restructured, and move to new domains constantly, and yet browsing still works, because the “API contract” between your browser and any website is remarkably thin: fetch a URL, receive HTML, look for links, follow one. HATEOAS asks: what if machine clients worked the exact same way against machine APIs?
A brief timeline
REST is born
Roy Fielding publishes his PhD dissertation defining REST, including the HATEOAS constraint, as a generalized description of the web’s architecture.
“REST” the buzzword
“REST” becomes a popular buzzword for HTTP+JSON APIs; most implementations skip hypermedia entirely.
Fielding pushes back
Fielding writes his widely cited blog post “REST APIs must be hypertext‑driven,” publicly criticizing APIs that call themselves REST without HATEOAS.
Maturity model
Leonard Richardson formalizes the Richardson Maturity Model (Levels 0‑3) at a QCon talk, giving the industry a shared vocabulary for grading REST maturity.
HAL emerges
Mike Kelly proposes the HAL (Hypertext Application Language) media type as a lightweight, practical hypermedia format.
JSON:API standardises
The JSON:API specification is published, formalizing conventions for hypermedia‑aware JSON responses including relationships and pagination links.
Frameworks mature
Spring HATEOAS and similar libraries mature across ecosystems (Java, .NET, Node), making implementation dramatically less manual.
A domain‑specific practice
HATEOAS remains a minority practice industry‑wide, but is standard in specific domains — payments, banking, travel booking — where long‑lived, multi‑party workflows benefit most from it.
The Problem & Motivation
To understand why HATEOAS exists, you first need to feel the pain it solves. Imagine you’re building a client application against a typical “REST” API for an order management system.
The tight‑coupling problem
Your client code probably looks like this today, scattered across dozens of files:
// Client-side code, hard-codes every URL and workflow rule
String orderUrl = "https://api.shop.com/orders/" + orderId;
String cancelUrl = "https://api.shop.com/orders/" + orderId + "/cancel";
String payUrl = "https://api.shop.com/orders/" + orderId + "/pay";
// The client must "know" — from reading documentation —
// that cancellation is only allowed when order.status == "PENDING"
if (order.getStatus().equals("PENDING")) {
httpClient.post(cancelUrl);
}Two things are silently baked into the client here:
- URL structure knowledge — the client assembles URLs by string concatenation, assuming the server’s routing will never change.
- Business rule knowledge — the client independently re‑implements the rule “orders can only be cancelled while PENDING,” duplicating logic that already lives on the server.
Now suppose the backend team refactors the URL scheme, or adds a new intermediate state AWAITING_PAYMENT_CONFIRMATION during which cancellation should also be blocked. Every client — web app, mobile app, third‑party integrator — has to be updated, recompiled, and redeployed, or it will silently do the wrong thing.
Without HATEOAS, the contract between client and server isn’t just the data shape — it silently includes URL structure and workflow/state rules. That triples the surface area of “breaking changes” and is the single biggest reason API versioning becomes painful at scale.
What HATEOAS proposes instead
The server, at runtime, tells the client exactly what it’s allowed to do next, based on the current state of the resource — and the client simply follows the links it’s given, the way a browser follows an <a href> tag without knowing anything about the target page in advance.
Analogy
An ATM machine doesn’t show you a “Withdraw” button if your balance is zero — the machine dynamically decides which buttons to present based on your account state. You never had to memorize “withdrawal is disabled below balance X”; the machine tells you in the moment.
Beginner example
A food delivery app shows a “Cancel Order” button only while your order is being prepared. Once it’s out for delivery, the button disappears — the app didn’t hard‑code that rule; the server told the app which actions are currently valid.
The versioning amplifier effect
This tight coupling problem doesn’t stay contained — it actively makes API versioning worse. When every client independently hard‑codes URLs and workflow rules, even a small backend change (renaming a path segment, adding a new precondition to an existing action) forces the backend team to either maintain the old behavior forever behind a version flag, or coordinate a synchronized release across every consuming team simultaneously. In large organizations with dozens of internal consumers, or public APIs with thousands of external integrators, this coordination cost becomes the dominant cost of evolving the API at all — teams start avoiding necessary refactors purely because the blast radius of a URL or workflow change is too large to manage. HATEOAS narrows that blast radius by keeping the two riskiest pieces of knowledge — exact URLs and exact state‑transition rules — inside the server, where a single team can change them safely.
Software example
A banking API without HATEOAS ships a mobile app that hard‑codes “loan approval requires a credit check status of APPROVED.” Six months later the bank adds a new intermediate MANUAL_REVIEW status — every installed app now behaves incorrectly until users update, which can take months across a large user base.
Production example
Stripe deliberately avoids this exact problem for its Payment Intents API by keeping the state machine server‑side and documenting it thoroughly, accepting the trade‑off of tighter documentation‑client coupling in exchange for a simpler, predictable Level 2 API surface — illustrating that both HATEOAS and disciplined Level 2 design are valid ways to manage this same underlying risk.
Core Concepts
Before diving into implementation, it helps to build a solid vocabulary. This section walks through each foundational concept, one at a time, with the definitions, motivations and examples you need to reason about HATEOAS confidently.
3.1 · The Richardson Maturity Model
Leonard Richardson proposed a widely‑used model that grades APIs on how “RESTful” they truly are, on a scale of Level 0 to Level 3. HATEOAS is precisely what separates Level 2 from Level 3.
The overwhelming majority of production “REST APIs” — including most internal microservice APIs at large companies — stop at Level 2. That’s not necessarily wrong (we’ll cover the trade‑offs later), but it’s important to know precisely where you stand and why.
3.2 · Hypermedia
Hypermedia simply means media (a document/response) that contains links to other media. HTML is the original hypermedia format — every webpage is hypermedia because it links to other webpages. HATEOAS takes that same idea and applies it to API responses: a JSON or XML document that contains not just data, but also links describing related and next‑step resources.
3.3 · Application State vs. Resource State
This is the concept most people get wrong. There are two different “states” in play:
| Term | Meaning | Example |
|---|---|---|
| Resource state | The data of a resource, stored server‑side | An order’s status field: PENDING |
| Application state | Where the client currently is in a multi‑step workflow | “I’ve viewed the order, I’m now deciding whether to pay or cancel” |
HATEOAS says the server should drive the client’s application state transitions by exposing valid links based on the current resource state. The client never needs a state machine diagram memorized in its own code — it just follows the links the server currently offers.
3.4 · Self‑Descriptive Messages
A closely related REST constraint: every response should carry enough metadata (media type, link relations) that a generic client can process it correctly without out‑of‑band documentation. HATEOAS strengthens this — the response doesn’t just say “here is data,” it says “here is data, and here is what you may legally do with it right now.”
3.5 · Link Relations (rel)
Every link in a HATEOAS response carries a relation type (rel) describing what that link means, not just where it points. IANA maintains an official registry of standard link relations (self, next, prev, edit, collection), and APIs can define custom ones (cancel, approve, pay).
Software example
GitHub’s API returns a Link header on paginated results with rel="next" and rel="prev" URLs — clients paginate by following links, never by manually computing offsets.
Production example
PayPal’s Payments API returns an approve link in the payment creation response — the client redirects the buyer to that exact URL rather than constructing PayPal’s checkout URL itself.
3.6 · The “generic client” test
A good way to judge whether an API is genuinely hypermedia‑driven is to ask: could a completely generic client — one that knows nothing about your specific business domain, only the hypermedia format itself — successfully use this API? A generic HAL client, for instance, knows how to parse _links and follow a URL for a given rel, but it has zero built‑in knowledge of what “cancel” or “pay” mean. It could still render a list of available actions by their relation names and let a human decide which to trigger. If your API instead requires the client to already know, out of band, which specific endpoint to call under which specific business condition, you’ve failed the generic client test — you’ve built Level 2, not Level 3.
3.7 · Affordances
Borrowed from design theory, an affordance is a property of an object that suggests how it can be used — a door handle affords pulling, a flat push‑plate affords pushing. In hypermedia APIs, a link with an attached HTTP method and expected input fields is an affordance: it doesn’t just say “here is a related resource,” it says “here is an action you can perform, here is how to perform it, and here is what input it expects.” Siren and HAL‑FORMS are hypermedia formats built specifically to carry rich affordance information, going a step beyond a bare URL.
3.8 · Uniform interface constraint, restated
Fielding grouped HATEOAS under REST’s broader uniform interface constraint, which itself has four sub‑constraints: identification of resources (URLs), manipulation of resources through representations, self‑descriptive messages, and hypermedia as the engine of application state. All four exist to keep the interface between any client and any server generic and stable, so that components can evolve independently — which is precisely the scalability property that let the web itself grow to billions of independently‑operated sites without a central coordinating authority.
Architecture & Components
A HATEOAS‑driven API response is architecturally composed of a small number of building blocks, regardless of which hypermedia format you choose.
4.1 · The anatomy of a hypermedia response
{
"orderId": "ORD-9001",
"status": "PENDING",
"totalAmount": 2499.00,
"currency": "INR",
"_links": {
"self": { "href": "/api/orders/ORD-9001" },
"cancel": { "href": "/api/orders/ORD-9001/cancel", "method": "POST" },
"pay": { "href": "/api/orders/ORD-9001/pay", "method": "POST" },
"items": { "href": "/api/orders/ORD-9001/items" },
"customer": { "href": "/api/customers/CUST-42" }
}
}Notice: if the order were instead in SHIPPED status, the cancel and pay links would simply be absent from the response — and a well‑built client wouldn’t even render a “Cancel” button, because there’s nothing to click.
4.2 · Hypermedia formats / media types
| Format | Media Type | Notes |
|---|---|---|
| HAL (Hypertext Application Language) | application/hal+json | Simplest, most widely adopted; uses _links and _embedded |
| JSON:API | application/vnd.api+json | Stricter spec, strong conventions for relationships & pagination |
| Siren | application/vnd.siren+json | Adds “actions” with input field descriptions — closest to true affordances |
| Collection+JSON | application/vnd.collection+json | Designed around read/write collections of items |
| ATOM/XML + XLink | application/atom+xml | Older, pre‑JSON‑era hypermedia format |
4.3 · Key components in a Spring Boot implementation
- RepresentationModel — the Spring HATEOAS base class your DTOs extend to gain a
_linkscollection. - Link / Affordance — objects describing a URI, a relation (rel), and optionally an HTTP method and expected input.
- LinkBuilder (WebMvcLinkBuilder) — a fluent helper that builds links from controller method references, so links stay in sync automatically if a route changes.
- RepresentationModelAssembler — a dedicated class responsible for converting a domain entity into a hypermedia‑enriched DTO, keeping link‑building logic out of your controllers.
_links.4.4 · The same resource in different hypermedia formats
To make the format differences concrete, here is the same order resource expressed three different ways.
{
"orderId": "ORD-9001",
"status": "PENDING",
"_links": {
"self": { "href": "/api/orders/ORD-9001" },
"cancel": { "href": "/api/orders/ORD-9001/cancel" }
}
}{
"data": {
"type": "orders",
"id": "ORD-9001",
"attributes": { "status": "PENDING" },
"relationships": {
"customer": {
"links": { "related": "/api/customers/CUST-42" }
}
},
"links": { "self": "/api/orders/ORD-9001" }
}
}{
"class": ["order"],
"properties": { "orderId": "ORD-9001", "status": "PENDING" },
"actions": [
{
"name": "cancel-order",
"method": "POST",
"href": "/api/orders/ORD-9001/cancel"
},
{
"name": "pay-order",
"method": "POST",
"href": "/api/orders/ORD-9001/pay",
"fields": [
{ "name": "paymentMethod", "type": "text" }
]
}
],
"links": [ { "rel": ["self"], "href": "/api/orders/ORD-9001" } ]
}Siren’s actions array is the closest thing to a true “affordance” as described in section 3.7 — a generic Siren client could render an actual form from the fields array without knowing anything about orders in advance.
Internal Working, Data Flow & Lifecycle
This chapter walks through building a HATEOAS API end to end in Spring Boot, then traces the exact request/response lifecycle it produces — from the entity in the database to the link a client eventually follows.
5.1 · Building a HATEOAS API step by step (Spring Boot)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>public class Order {
private String orderId;
private String status; // PENDING, PAID, SHIPPED, CANCELLED
private BigDecimal totalAmount;
// getters/setters omitted for brevity
}public class OrderModel extends RepresentationModel<OrderModel> {
private String orderId;
private String status;
private BigDecimal totalAmount;
// getters/setters
}@Component
public class OrderModelAssembler
implements RepresentationModelAssembler<Order, OrderModel> {
@Override
public OrderModel toModel(Order order) {
OrderModel model = new OrderModel();
model.setOrderId(order.getOrderId());
model.setStatus(order.getStatus());
model.setTotalAmount(order.getTotalAmount());
// Always include a self link
model.add(linkTo(methodOn(OrderController.class)
.getOrder(order.getOrderId())).withSelfRel());
// Conditionally add links based on CURRENT resource state —
// this IS the "engine of application state"
if ("PENDING".equals(order.getStatus())) {
model.add(linkTo(methodOn(OrderController.class)
.cancelOrder(order.getOrderId())).withRel("cancel"));
model.add(linkTo(methodOn(OrderController.class)
.payOrder(order.getOrderId())).withRel("pay"));
}
if ("SHIPPED".equals(order.getStatus())) {
model.add(linkTo(methodOn(OrderController.class)
.trackOrder(order.getOrderId())).withRel("track"));
}
return model;
}
}@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final OrderModelAssembler assembler;
public OrderController(OrderService orderService, OrderModelAssembler assembler) {
this.orderService = orderService;
this.assembler = assembler;
}
@GetMapping("/{id}")
public ResponseEntity<OrderModel> getOrder(@PathVariable String id) {
Order order = orderService.findById(id);
return ResponseEntity.ok(assembler.toModel(order));
}
@PostMapping("/{id}/cancel")
public ResponseEntity<OrderModel> cancelOrder(@PathVariable String id) {
Order updated = orderService.cancel(id);
return ResponseEntity.ok(assembler.toModel(updated));
}
@PostMapping("/{id}/pay")
public ResponseEntity<OrderModel> payOrder(@PathVariable String id) {
Order updated = orderService.pay(id);
return ResponseEntity.ok(assembler.toModel(updated));
}
}Notice the controller never mentions link logic at all — it doesn’t know or care which links apply. All state‑to‑link decisions live in exactly one place (the assembler), so when your business rules change (e.g. a new “AWAITING_PAYMENT_CONFIRMATION” state), you edit one method instead of hunting through every controller and every client.
5.2 · Request / response lifecycle with HATEOAS
This is the essence of “hypermedia as the engine of application state” — the server, not client‑side business logic, dictates what transitions are currently legal, turn by turn, response by response.
5.3 · Content negotiation and media types
A properly self‑descriptive HATEOAS response sets its Content-Type to a hypermedia‑aware media type:
HTTP/1.1 200 OK
Content-Type: application/hal+json
{ "orderId": "ORD-9001", "status": "PENDING", "_links": { ... } }5.4 · A minimal generic client, to see it from the other side
To fully close the loop, here’s a tiny illustrative client that never hard‑codes a single action URL — it discovers everything by following relation names, exactly as described in section 3.6.
public class OrderClient {
private final RestTemplate restTemplate;
public void processOrder(String entryPointUrl) {
// Start from ONE known URL — the entry point
Map<String, Object> order = restTemplate.getForObject(entryPointUrl, Map.class);
Map<String, Object> links = (Map<String, Object>) order.get("_links");
// Never hard-code "/orders/{id}/pay" — look it up by relation name
if (links.containsKey("pay")) {
String payUrl = extractHref(links.get("pay"));
restTemplate.postForObject(payUrl, null, Map.class);
} else {
System.out.println("Payment is not currently available for this order.");
}
}
private String extractHref(Object linkObj) {
return (String) ((Map<String, Object>) linkObj).get("href");
}
}Notice this client would keep working correctly even if the backend team moved the pay endpoint from /api/orders/{id}/pay to /api/v2/payments?order={id} — because the client never knew or cared about the URL shape, only the relation name pay.
5.5 · Testing hypermedia APIs
Testing strategy needs to shift focus slightly compared to plain REST testing:
- State‑transition tests — for every meaningful resource state, assert the exact set of
relvalues present (and explicitly assert the ones that must be absent, e.g. nocancellink on a shipped order). - Link resolvability tests — follow every returned link in an integration test and assert it resolves to a non‑error response, catching broken links from route refactors early.
- Contract tests per relation — using a tool like Pact, define consumer expectations in terms of relation names and their expected method/fields, not raw URL strings.
@Test
void pendingOrderExposesCancelAndPayLinks() throws Exception {
mockMvc.perform(get("/api/orders/ORD-9001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.cancel.href").exists())
.andExpect(jsonPath("$._links.pay.href").exists());
}
@Test
void shippedOrderHidesCancelAndPayLinks() throws Exception {
mockMvc.perform(get("/api/orders/ORD-9002")) // ORD-9002 is SHIPPED
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.cancel").doesNotExist())
.andExpect(jsonPath("$._links.pay").doesNotExist())
.andExpect(jsonPath("$._links.track.href").exists());
}5.6 · Versioning implications
HATEOAS doesn’t remove the need for API versioning, but it does change what typically forces a version bump. Renaming a field is still breaking, exactly as before. But adding a brand‑new optional link relation is usually non‑breaking, since well‑built clients simply ignore relation names they don’t recognize — this is one of hypermedia’s quieter benefits: it’s easier to extend an API’s capabilities without a version bump, as long as you’re strict about never repurposing an existing relation name to mean something new.
Pros, Cons & Trade‑offs
HATEOAS is not free. It buys you real, measurable properties, and it costs you real, measurable effort — and choosing well requires being honest about both sides.
Advantages
- Clients decouple from URL structure — the server can rename/move endpoints without breaking clients that follow links instead of hard‑coding them.
- Business/workflow rules live in one place (the server) instead of being duplicated in every client.
- Discoverability — new capabilities can be added and immediately used by generic hypermedia‑aware clients without a redeploy.
- Reduces “invalid state” bugs — if a link isn’t offered, the action was never possible, removing a whole class of client‑side validation code.
- Naturally self‑documenting at runtime; reduces drift between docs and actual behavior.
Disadvantages
- Significant added complexity in server‑side code (assemblers, link builders, media type negotiation).
- Larger response payloads — every response now carries link metadata, which matters at high scale/low latency budgets.
- Tooling and client‑library support is weaker than plain JSON — most frontend teams still hard‑code URLs even when links are provided.
- Steeper learning curve for teams; genuinely few working examples to copy from in most companies.
- Doesn’t eliminate the need for documentation — clients still need to know link relation names (rel values) up front, so some coupling always remains.
The honest trade‑off
HATEOAS shifts coupling from “URL structure and workflow rules” down to “link relation vocabulary” — it doesn’t eliminate coupling entirely, it moves it to a smaller, more stable surface. That’s a genuinely good trade for large, long‑lived, multi‑team, or public APIs (banking, e‑commerce, travel booking). It’s often not worth the complexity for small internal CRUD services with one client team that ships in lockstep with the backend.
Performance & Scalability
HATEOAS has real, measurable performance implications that architects must plan for.
Payload size
Every link adds bytes. A response with 6‑8 links can easily be 30‑50% larger than the bare data. At high request volumes, this directly increases bandwidth cost and serialization/deserialization CPU time.
Mitigations
- Selective link inclusion — only compute and attach links that are actually valid for the current state, rather than always generating the full set and filtering client‑side.
- Compression — enable gzip/br response compression; link URLs compress extremely well due to repetition.
- Field/link projection — support a query parameter (e.g.
?links=minimal) for high‑throughput internal callers who don’t need hypermedia at all. - Lazy link computation — build links only after confirming the client’s
Acceptheader actually requests a hypermedia media type.
Some teams compute links by making internal service calls to check permissions (“can this user cancel this order?”) for every possible link, on every single response. At scale this multiplies backend calls by the number of link candidates. Precompute permission checks in bulk or cache them per request context.
High Availability & Reliability
HATEOAS interacts with HA/DR concerns mainly through URL stability across environments and failover.
- Absolute vs relative URLs during failover — if your API generates fully‑qualified links (
https://api.shop.com/...) and traffic fails over to a DR region behind a different hostname, stale links can point clients at a dead node. Prefer building links from the incoming request’sHost/X-Forwarded-Hostheader, or route everything through a stable API gateway hostname. - Idempotency on followed links — action links like
cancelorpayshould be safe to retry (idempotency keys) since clients following links during a network blip may resend the same POST. - Graceful degradation — if the link‑computation subsystem itself fails (e.g. a permission service is down), fail safely by omitting the affected link rather than crashing the whole response — the client simply won’t see that action, which is a safe default.
- Circuit breakers around link‑dependency calls — if link generation requires calling another microservice (e.g. checking payment eligibility before including a “pay” link), wrap that call in a circuit breaker (Resilience4j) with a sensible fallback of “omit the link” rather than letting a slow downstream dependency block the entire resource response.
The general principle across all of these is that HATEOAS should degrade gracefully by omission, never by failure. A resource missing an optional link is a minor UX inconvenience; a resource that fails to render at all because a non‑critical link couldn’t be computed is a much more serious availability regression, and one that’s entirely avoidable with the right fallback design.
Security
HATEOAS actually offers a genuine security benefit if used correctly, alongside some new risks to manage.
The benefit · capability‑based access control
Because links are only included in the response when the current user is authorized and the resource is in a valid state, a well‑built client naturally never even attempts unauthorized actions — the “Cancel” button is never rendered for a user without cancel permission, and never for an order that’s already shipped.
The absence of a link in a response is a UX hint, not an access‑control boundary. A malicious client can simply construct the URL manually and call it directly. The server must independently re‑validate authorization and state on every single request, exactly as it would without HATEOAS.
Other security considerations
- Information disclosure — don’t expose links to internal‑only or admin‑only actions to unauthorized users; compute the link set per‑caller, not globally per‑resource.
- URL tampering / IDOR — links reduce (but don’t remove) the risk of clients guessing sequential IDs, since valid workflows are discovered via links. Still enforce object‑level authorization server‑side (classic OWASP API Top 10 concern — Broken Object Level Authorization).
- Signed/expiring action URLs — for sensitive one‑time actions (e.g. password reset, payment approval), consider embedding a short‑lived signed token in the link itself so the URL cannot be replayed indefinitely.
Mapping to OWASP API Security Top 10
It’s worth explicitly connecting HATEOAS practice to the OWASP API Security Top 10 list, since architects are often asked to justify design decisions against it during security reviews:
| OWASP API Risk | How HATEOAS relates |
|---|---|
| Broken Object Level Authorization | Link presence must never substitute for server‑side per‑object authorization checks — HATEOAS can reduce accidental exposure in UI but doesn’t fix this on its own. |
| Broken Function Level Authorization | Well‑implemented link computation (only show admin-approve to admins) directly helps here, provided the underlying endpoint is also independently protected. |
| Excessive Data Exposure | Risk increases if link generation logic accidentally reveals internal‑only relations or admin endpoints to unauthorized users — audit your assembler logic per role. |
| Improper Assets Management | Hypermedia’s self‑describing nature can help keep an inventory of “which actions exist and where” more accurate than static docs that drift out of sync. |
Monitoring, Logging & Metrics
Observability for a HATEOAS API needs a couple of extra dimensions beyond standard REST monitoring.
- Link relation usage metrics — instrument which
relvalues clients actually follow (e.g. via a custom Micrometer counter tagged by rel name). Unused link relations are candidates for removal; heavily‑used ones tell you where to invest in caching. - Payload size tracking — monitor response body size distributions (p50/p95/p99) since link bloat grows silently as more actions are added over time.
- Link generation latency — if link building involves permission checks or DB lookups, trace that span separately from core business logic in your APM (e.g. via OpenTelemetry spans named
build-links). - Broken link detection — periodic synthetic tests that fetch a resource, follow every returned link, and assert a non‑5xx response, catching drift between the assembler and actual routes.
@Component
public class LinkUsageInterceptor implements HandlerInterceptor {
private final MeterRegistry registry;
public LinkUsageInterceptor(MeterRegistry registry) {
this.registry = registry;
}
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String followedRel = req.getHeader("X-Followed-Rel"); // client sends this optionally
if (followedRel != null) {
registry.counter("hateoas.link.followed", "rel", followedRel).increment();
}
return true;
}
}10.1 · What to put on a HATEOAS‑specific dashboard
Beyond the generic API dashboard (latency, error rate, throughput by endpoint), a hypermedia‑aware API benefits from a few additional panels that most teams skip entirely until something goes wrong:
- Relation adoption over time — a chart of “% of requests to action X that arrived via a followed link vs. a manually constructed URL,” inferred from a custom header or referrer tracking. A low adoption number tells you client teams still aren’t trusting the hypermedia layer, which is useful signal for deciding whether to keep investing in it.
- Dead link alerts — a synthetic monitor (section 5.5’s link‑resolvability test, run continuously in production) wired to alert if any advertised link starts returning 404/500, since a dead link silently breaks every client following the contract correctly.
- State‑distribution histogram — how many resources currently sit in each state (PENDING, PAID, SHIPPED, etc.), useful context for interpreting which link relations should dominate traffic at any given time.
None of this replaces standard APM tooling (Datadog, New Relic, Prometheus + Grafana) — it’s an additional lens layered on top, specific to the promise a hypermedia API makes: that what it advertises as possible is always accurate and always reachable.
Deployment & Cloud Considerations
Where and how a HATEOAS API is deployed shapes the correctness of the very links it generates — gateways, ingress controllers and multi‑region routing all get involved.
- API Gateway link rewriting — when deployed behind an API gateway (Kong, AWS API Gateway, Spring Cloud Gateway) that changes the externally visible path prefix, ensure your link builder uses the gateway’s forwarded headers so generated links match the client‑facing URL, not the internal service URL.
- Blue‑green / canary deployments — since link relation names (
rel) form part of your contract, treat renaming or removing arelas a breaking change requiring the same versioning discipline as removing a field. - Multi‑region deployments — generate links relative to the region that served the request, or route all link hosts through a global anycast/CDN hostname to avoid cross‑region redirects.
- Containerized environments — in Kubernetes, ensure your Spring Boot app trusts
X-Forwarded-*headers set by the Ingress controller soServletUriComponentsBuilder‑based link generation reflects the public hostname, not the internal pod IP.
# application.yml
server:
forward-headers-strategy: framework
# Ensures Spring correctly rebuilds request.getScheme()/getServerName()
# from X-Forwarded-Proto / X-Forwarded-Host, so links generated via
# ServletUriComponentsBuilder.fromCurrentRequest() reflect the
# gateway's public-facing hostname rather than the internal pod address.Skipping this single configuration line is one of the most common causes of a subtle but frustrating production bug: an API that works perfectly in local testing suddenly returns links pointing at an internal, unreachable hostname once deployed behind a gateway or load balancer, because the framework built the link from the raw incoming connection instead of the forwarded headers.
Caching & Load Balancing
Even though HATEOAS is primarily an API design constraint rather than a data‑layer concern, it does touch caching directly.
- HTTP caching headers still apply — a hypermedia response is still a normal HTTP response, so
ETag,Cache-Control, andLast-Modifiedwork exactly as they do for plain JSON. Because the link set changes whenever resource state changes, the ETag naturally changes too — which is actually a nice property, since cache invalidation lines up perfectly with “the set of valid actions changed.” - CDN caching of GET responses with links — safe for public catalog‑style resources (e.g. a product listing with “next page” links) but avoid caching responses containing user‑specific action links (like a personalized “cancel” link) at a shared CDN layer; scope those to private/no‑store caching.
- Load balancer session affinity — not required by HATEOAS itself, since REST (and HATEOAS) mandates statelessness — every request must carry all context needed to process it, so any node behind the load balancer can serve any request without sticky sessions.
Because HATEOAS pushes “what can I do next” into the response itself rather than server‑side session memory, it reinforces REST’s statelessness constraint — the client never needs the server to “remember” where it is in a workflow; each response is self‑sufficient.
This also simplifies load balancer configuration in a very concrete, practical way: because no node needs to remember which step of a workflow a particular client reached, a simple round‑robin or least‑connections algorithm works just as well as anything more elaborate — there’s no need for the sticky‑session complexity (and its associated failover headaches) that stateful workflow designs often require.
APIs & Microservices
HATEOAS becomes especially relevant — and especially contested — in microservice architectures.
Cross‑service link composition
In a microservices system, a single hypermedia response for an “Order” resource might legitimately need to include links into the Payment service, the Shipping service, and the Customer service. This requires either:
- An API Gateway / Backend‑for‑Frontend (BFF) layer that aggregates and rewrites links from multiple downstream services into a single coherent hypermedia document, or
- Each service generating self‑contained links to itself only, with the client (or a gateway) responsible for stitching together a full workflow across services.
Service discovery synergy
HATEOAS pairs naturally with dynamic service discovery: instead of clients or even other services hard‑coding downstream URLs, they can start from a well‑known root/entry‑point resource and navigate outward via links, letting the topology change underneath them without any client changes — mirroring how a browser never hard‑codes IP addresses.
Contract testing implications
With HATEOAS, consumer‑driven contract tests (e.g. Pact) need to assert not just field presence, but the presence/absence of specific rel values under specific resource states — a stronger, more behavior‑aware contract than plain schema validation.
The aggregation trade‑off
Gateway‑level link aggregation (option one above) gives clients a single, coherent document but reintroduces a form of coupling: the gateway must know about every downstream service’s link shape, which can turn the gateway into a bottleneck for change, echoing the very problem HATEOAS set out to avoid, just one layer removed. Self‑contained per‑service linking (option two) keeps each service fully autonomous but pushes the burden of stitching a full cross‑service workflow onto the client, which partially undermines the “generic client” ideal from section 3.6. Most real systems land on a pragmatic middle ground: a thin BFF that aggregates only for the top few, highest‑value composite views (like an order summary page), while leaving deeper drill‑down navigation to direct per‑service hypermedia links.
Design Patterns & Anti‑Patterns
Every mature HATEOAS codebase converges on a handful of durable patterns — and, more importantly, on a specific set of tempting shortcuts that quietly undo everything you built the hypermedia layer to achieve.
Patterns
- Assembler / Converter Pattern — as shown in section 5, isolate link‑building logic in a dedicated class per resource type, never inline it in controllers.
- Affordance Pattern (Siren‑style) — go beyond a bare URL and describe the expected input schema for an action, so clients can render forms dynamically:
Affordance · action with input schema
{ "name": "pay", "method": "POST", "href": "/api/orders/ORD-9001/pay", "fields": [ { "name": "paymentMethod", "type": "text" }, { "name": "amount", "type": "number" } ] } - Root Resource / Entry Point Pattern — expose a single well‑known root URL (e.g.
/api) whose links bootstrap discovery of the entire API, mirroring how a website starts at its homepage. - Embedded Resources Pattern (HAL
_embedded) — include full related sub‑resources inline to save a round trip, while still providing their canonical link for later independent access.
Anti‑patterns
Adding a static _links block that always contains the same links regardless of resource state (e.g. always including “cancel” even for a shipped order). This isn’t HATEOAS — it’s decoration. The whole point is that the link set changes as state changes; if it doesn’t, you’ve added payload bytes with zero benefit.
Link explosion
Attaching dozens of speculative links “just in case,” bloating every response and drowning clients (and dashboards) in noise.
Undocumented rel vocabulary
Inventing custom relation names without publishing what they mean anywhere, defeating the self‑descriptive‑message goal.
Links as the only auth check
Treating link presence as the boundary for whether an action is allowed — covered in Security. A serious anti‑pattern with real security consequences.
Client‑side URL reconstruction
A client that receives links but still builds some URLs manually “because it’s faster” reintroduces the exact coupling HATEOAS was meant to remove.
Best Practices & Common Mistakes
A field‑guide checklist distilled from teams that ship hypermedia APIs successfully — and the surprisingly common failures that keep resurfacing in code reviews and post‑mortems.
Best practices
- Choose an established hypermedia format (HAL or JSON:API) instead of inventing your own
_linksshape. - Generate links from route definitions (e.g.
WebMvcLinkBuilder.linkTo(methodOn(...))) instead of string concatenation, so a route rename can’t silently produce a dead link. - Version your link relation vocabulary just like you version fields — publish a registry of custom
relnames and their meaning. - Always re‑validate authorization and state server‑side on the action itself, never trusting link presence alone.
- Start with a small, well‑chosen set of action links (the ones that actually represent state transitions) rather than linking every conceivable related resource.
- Provide an entry‑point/root resource so new clients can discover the API by following links from one known URL.
Common mistakes
- Building “fake” static links that don’t reflect actual resource state (see anti‑patterns above).
- Forgetting to update the assembler when a new business state is introduced, leaving stale or missing links.
- Ignoring payload size impact until it shows up as a real latency/cost problem in production.
- Assuming client teams will actually build generic hypermedia‑following UI — in practice, most frontend teams still hard‑code screens per action, so the ROI of full HATEOAS should be evaluated honestly against your actual client landscape.
- Mixing hypermedia and non‑hypermedia responses inconsistently across an API surface, confusing both humans and tooling.
A simple decision framework
Before committing to full HATEOAS, it helps to answer a few honest questions about your actual system:
| Question | Leans toward HATEOAS | Leans toward plain REST (Level 2) |
|---|---|---|
| How many independent client teams consume this API? | Many, including third parties you don’t control | One team, deployed together with the backend |
| How many valid states does a resource pass through? | Many (order lifecycle, loan approval, claim processing) | Few or none (simple CRUD entities) |
| How often do workflow rules change? | Frequently, and changes are hard to coordinate across clients | Rarely, and all clients ship together anyway |
| What’s the cost of a client attempting an invalid action? | High (financial transaction, irreversible operation) | Low (easily validated and rejected server‑side, low stakes) |
| Is response payload size a hard constraint? | No, some overhead is acceptable | Yes, every byte matters (very high QPS, mobile data limits) |
If most of your honest answers land in the left column, HATEOAS is likely worth its complexity. If most land in the right column, a well‑documented Level 2 REST API — clear resources, correct verbs, correct status codes — is probably the better engineering trade‑off, and adding hypermedia would be complexity without a corresponding payoff.
Real‑World / Industry Examples
Where HATEOAS actually shows up in production — and, equally instructively, where large teams have deliberately chosen not to use it.
PayPal
PayPal’s REST Payments API is one of the most cited real HATEOAS implementations — payment creation responses include approval_url, execute, and self links that the client is expected to follow rather than construct.
GitHub
GitHub’s REST API uses Link headers with rel="next"/rel="prev"/rel="last" for pagination across virtually every list endpoint, a lightweight but genuine hypermedia pattern.
Amazon S3
S3’s list‑objects API returns a NextContinuationToken that functions as an opaque hypermedia‑style cursor — clients don’t compute pagination offsets themselves.
Netflix
Netflix’s internal API gateway historically used hypermedia‑influenced device APIs so that hundreds of device types (TVs, consoles, phones) could adapt to backend changes without every device team shipping a new build.
Where it’s deliberately NOT used
Many high‑throughput public APIs — Stripe, Twilio, Google Cloud APIs — are explicitly Level 2 (Richardson Maturity Model), not Level 3. They prioritize simple, predictable, well‑documented fixed endpoints over hypermedia discoverability, because their client base overwhelmingly consists of developers reading fixed documentation and generating typed SDK clients, where hypermedia’s dynamic discovery adds complexity without matching benefit. This is a legitimate, deliberate architectural choice, not an oversight.
Why payments and banking APIs lean toward HATEOAS
It’s worth pausing on why the payments/banking domain keeps showing up in real HATEOAS examples — it isn’t a coincidence. Payment workflows are inherently multi‑step, involve external redirects to third parties (a bank’s own authentication page, a 3‑D Secure challenge screen), and the valid next step genuinely depends on server‑side state that can change for reasons the client can’t predict (fraud checks, bank approval delays, partial captures). That combination — long‑lived workflows, externally‑injected state changes, and serious cost‑of‑error if a client attempts an invalid transition — is exactly the profile where hypermedia‑driven APIs earn their complexity budget. A simple CRUD “update user profile” endpoint almost never has that profile, which is a big part of why HATEOAS adoption correlates so strongly with domain, not company size or technical sophistication.
Comparison · how different API styles handle discoverability
| Style | How clients learn what’s possible | Typical use case |
|---|---|---|
| RPC / Level 0‑1 REST | Static documentation, read once at development time | Simple internal services, single client team |
| Level 2 REST (most “REST APIs”) | OpenAPI/Swagger spec + fixed endpoint documentation | Public developer‑facing APIs with SDKs (Stripe, Twilio) |
| HATEOAS / Level 3 REST | Runtime links in each response, discovered dynamically | Long‑lived multi‑step workflows, payments, multi‑party integrations |
| GraphQL | Introspectable schema, client asks for exact fields needed | Aggregating many backend sources for flexible frontend needs |
| gRPC | Compiled Protobuf contract, generated client stubs | High‑performance internal service‑to‑service calls |
Frequently Asked Questions
Ten questions we hear most often once teams start seriously evaluating HATEOAS for a real system.
Is an API “not REST” if it doesn’t implement HATEOAS?
By Roy Fielding’s strict original definition, yes — HATEOAS is a mandatory REST constraint, and an API without it is more accurately RPC‑over‑HTTP. In everyday industry usage, though, “REST API” has come to loosely mean any HTTP+JSON API using proper verbs and status codes (Richardson Level 2), and that usage isn’t going away. Use precise terminology when it matters (architecture reviews, academic contexts) and be pragmatic elsewhere.
Do I need Spring HATEOAS, or can I just add a “links” field manually?
You can absolutely hand‑roll a links field for a small API. Spring HATEOAS earns its keep once you have many resource types and states, because linkTo(methodOn(...)) keeps links in sync with actual controller routes automatically, preventing dead links after refactors.
Does HATEOAS replace API documentation (like OpenAPI/Swagger)?
No. You still need to document your link relation vocabulary, media types, and overall resource model up front. HATEOAS changes when a client learns which specific actions are valid right now (at runtime, per resource) — it doesn’t remove the need to document what a “cancel” relation generally means.
Is GraphQL a replacement for HATEOAS?
They solve different problems. GraphQL lets clients query exactly the fields they need in one round trip; it doesn’t inherently communicate valid state transitions. Some teams do add mutation‑availability fields to GraphQL schemas to approximate HATEOAS‑like discoverability, but it isn’t a built‑in GraphQL constraint.
Should every microservice implement HATEOAS?
Not necessarily. It’s most valuable at boundaries with many independent, loosely‑coordinated client teams (public APIs, partner integrations) or long‑lived workflows with many valid states. For tightly coupled internal services deployed together by one team, the added complexity often isn’t worth it — plain versioned REST or even RPC (gRPC) may be simpler and faster.
What’s the performance cost in practice?
Typically a 10‑50% payload size increase depending on how many links you attach, plus modest CPU for link generation. For most APIs this is negligible next to database and network latency, but it should be measured, not assumed, especially for very high‑QPS internal services.
How is HATEOAS different from just returning a list of “related IDs”?
Returning "customerId": "CUST-42" tells the client a related entity exists, but the client still has to know, from documentation, how to build a URL to fetch it and whether that fetch is even currently allowed. A HATEOAS link ("customer": { "href": "/api/customers/CUST-42" }) removes both of those assumptions — the URL is handed to the client directly, and its mere presence already implies the action is currently valid.
Can HATEOAS work with mobile apps, given app store release cycles are slow?
Yes, and arguably it matters more there. Because native mobile clients can’t be patched instantly the way a web frontend can, hard‑coded business rules baked into an app binary are especially costly to fix. A mobile client that follows server‑driven links can adapt to backend workflow changes without an app store release, as long as its UI logic is written generically enough to react to “is this link present or not” rather than to specific hard‑coded screens.
What is HAL‑FORMS and how does it differ from plain HAL?
HAL‑FORMS is an extension to HAL that adds a _templates section describing the expected input fields, data types, and validation rules for each action‑link — essentially bringing Siren‑style affordances to the HAL ecosystem, letting a generic client render an actual input form instead of just knowing a URL exists.
Does HATEOAS apply to gRPC or event‑driven (Kafka) APIs?
Not directly — HATEOAS is a constraint specifically for hypertext‑style request/response HTTP APIs. gRPC uses a compiled, strongly‑typed contract (via Protocol Buffers) that clients generate code from ahead of time, which is architecturally closer to Level 0/1 in spirit, prioritizing performance and type safety over runtime discoverability. Event‑driven systems have their own analogous discoverability concerns (schema registries, event catalogs) but they aren’t described using hypermedia link relations.
Summary & Key Takeaways
HATEOAS is REST’s most demanding — and most frequently skipped — constraint. It asks the server to actively drive the client’s next possible actions through hypermedia links embedded in every response, rather than the client hard‑coding URL structure and workflow rules from static documentation.
Key Takeaways
- HATEOAS = Hypermedia As The Engine Of Application State, one of REST’s original constraints from Roy Fielding’s 2000 dissertation.
- It is the dividing line between Richardson Maturity Model Level 2 (most “REST APIs” today) and true Level 3 REST.
- The core mechanism: a resource’s response only includes links for actions that are currently valid, given its current state.
- Real benefit: decouples clients from URL structure and duplicated business rules, reducing breaking changes and invalid‑state bugs.
- Real cost: added server‑side complexity, larger payloads, and weak client tooling support industry‑wide.
- Spring HATEOAS’s assembler pattern is the standard way to implement this cleanly in Java — isolate state‑to‑link decisions in one class per resource.
- Never treat link presence as your only security boundary — always re‑validate authorization and state server‑side, independently.
- Adopt it deliberately for large, long‑lived, multi‑client, or public APIs; it’s often overkill for small, tightly‑coupled internal services.
HATEOAS turns your API responses into a living map of “what you can do right now,” so clients navigate by following signposts the server puts up in real time — instead of memorizing a static route map that breaks the moment the road changes.
If you take away one practical action from this guide, let it be this: before your next API design review, run the resources you’re building through the decision framework in section 15. If the answers point toward HATEOAS, invest early — retrofitting hypermedia onto an API that already has thousands of clients hard‑coding URLs is far more expensive than designing it in from day one. And if the answers point away from HATEOAS, that’s a perfectly legitimate outcome too — just make sure your team can articulate it as a deliberate choice, not an accident of skipping a chapter in Fielding’s dissertation.