What is HATEOAS in REST APIs?

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.

01

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.

A useful mental anchor

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

2000

REST is born

Roy Fielding publishes his PhD dissertation defining REST, including the HATEOAS constraint, as a generalized description of the web’s architecture.

2007‑08

“REST” the buzzword

“REST” becomes a popular buzzword for HTTP+JSON APIs; most implementations skip hypermedia entirely.

2008

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.

2010

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.

2011

HAL emerges

Mike Kelly proposes the HAL (Hypertext Application Language) media type as a lightweight, practical hypermedia format.

2013

JSON:API standardises

The JSON:API specification is published, formalizing conventions for hypermedia‑aware JSON responses including relationships and pagination links.

2013‑15

Frameworks mature

Spring HATEOAS and similar libraries mature across ecosystems (Java, .NET, Node), making implementation dramatically less manual.

Present

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.

02

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:

Typical hard‑coded client · the problem
// 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.

!
This is the core failure mode

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.

03

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.

Level 0 · The Swamp of POX single URL · single HTTP verb (usually POST) · SOAP-style RPC over HTTP Level 1 · Resources multiple URLs, one per resource · /orders/123, /customers/45 Level 2 · HTTP Verbs proper GET / POST / PUT / DELETE · correct status codes Level 3 · Hypermedia Controls (HATEOAS) responses contain links describing next possible actions — the goal — START RESTFUL
Figure 1 · The Richardson Maturity Model. HATEOAS is the specific constraint that moves an API from Level 2 (correct verbs and status codes) up to Level 3 (hypermedia‑driven).

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:

TermMeaningExample
Resource stateThe data of a resource, stored server‑sideAn order’s status field: PENDING
Application stateWhere 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.

04

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

Order resource with HATEOAS links · HAL format
{
  "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

FormatMedia TypeNotes
HAL (Hypertext Application Language)application/hal+jsonSimplest, most widely adopted; uses _links and _embedded
JSON:APIapplication/vnd.api+jsonStricter spec, strong conventions for relationships & pagination
Sirenapplication/vnd.siren+jsonAdds “actions” with input field descriptions — closest to true affordances
Collection+JSONapplication/vnd.collection+jsonDesigned around read/write collections of items
ATOM/XML + XLinkapplication/atom+xmlOlder, 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 _links collection.
  • 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.
Domain Entity Order.java Assembler OrderModelAssembler RepresentationModel OrderModel + _links Controller ResponseEntity Client follows _links
Figure 2 · The Spring HATEOAS component pipeline: a domain entity is converted by an assembler into a RepresentationModel carrying links, returned by a thin controller, and consumed by a client that navigates purely via _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.

HAL · compact, widely supported
{
  "orderId": "ORD-9001",
  "status": "PENDING",
  "_links": {
    "self":   { "href": "/api/orders/ORD-9001" },
    "cancel": { "href": "/api/orders/ORD-9001/cancel" }
  }
}
JSON:API · stricter, built around “type” + “relationships”
{
  "data": {
    "type": "orders",
    "id": "ORD-9001",
    "attributes": { "status": "PENDING" },
    "relationships": {
      "customer": {
        "links": { "related": "/api/customers/CUST-42" }
      }
    },
    "links": { "self": "/api/orders/ORD-9001" }
  }
}
Siren · carries full “actions” with expected input fields
{
  "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.

05

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)

Maven dependency
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
Domain entity
public class Order {
    private String orderId;
    private String status;      // PENDING, PAID, SHIPPED, CANCELLED
    private BigDecimal totalAmount;
    // getters/setters omitted for brevity
}
Representation model · the DTO that carries links
public class OrderModel extends RepresentationModel<OrderModel> {
    private String orderId;
    private String status;
    private BigDecimal totalAmount;
    // getters/setters
}
The assembler · deciding WHICH links to attach based on state
@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;
    }
}
The controller · thin, delegates hypermedia decisions to the assembler
@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));
    }
}
Why the assembler pattern matters

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

Client Server GET /api/orders/ORD-9001 200 OK · { status: PENDING, _links: {self, cancel, pay} } Client renders “Cancel” and “Pay” buttons because those links exist POST /api/orders/ORD-9001/pay (follows “pay” link) 200 OK · { status: PAID, _links: {self, track} } “Cancel” / “Pay” buttons disappear; “Track” button now appears — the server drove the transition
Figure 3 · The request/response lifecycle with HATEOAS. The link set advertised by each response is exactly the set of transitions currently legal for that resource’s state — and it changes turn by turn, driven entirely by the server.

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 · hypermedia response
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.

Generic client following links by relation name · Java
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 rel values present (and explicitly assert the ones that must be absent, e.g. no cancel link 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.
Spring test asserting conditional links by state
@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.

06

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.

HATEOAS doesn’t eliminate coupling — it moves it from a fragile surface (URLs, workflow rules) to a much smaller and more stable one (a vocabulary of relation names).
07

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 Accept header actually requests a hypermedia media type.
!
Common scaling mistake

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.

30‑50%
Typical payload size increase from link metadata
gzip/br
Link URLs compress extremely well due to repetition
Lazy
Build links only when Accept requests hypermedia
Bulk
Cache per‑request permission checks, don’t multiply calls
08

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’s Host/X-Forwarded-Host header, or route everything through a stable API gateway hostname.
  • Idempotency on followed links — action links like cancel or pay should 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.

09

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.

!
Never rely on link presence as your only security control

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 RiskHow HATEOAS relates
Broken Object Level AuthorizationLink 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 AuthorizationWell‑implemented link computation (only show admin-approve to admins) directly helps here, provided the underlying endpoint is also independently protected.
Excessive Data ExposureRisk increases if link generation logic accidentally reveals internal‑only relations or admin endpoints to unauthorized users — audit your assembler logic per role.
Improper Assets ManagementHypermedia’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.
10

Monitoring, Logging & Metrics

Observability for a HATEOAS API needs a couple of extra dimensions beyond standard REST monitoring.

  • Link relation usage metrics — instrument which rel values 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.
Example · tagging a Micrometer counter by link relation
@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.

11

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 a rel as 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 so ServletUriComponentsBuilder‑based link generation reflects the public hostname, not the internal pod IP.
Spring Boot config to trust forwarded headers behind a proxy/gateway
# 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.

12

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, and Last-Modified work 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.
Statelessness + HATEOAS work together

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.

13

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.
Client browser / mobile API Gateway / BFF aggregates & rewrites links Order Service emits self + cross-service links Payment Service receives followed links Shipping Service receives followed links links to links to
Figure 4 · Cross‑service link composition: the Order service’s hypermedia response includes links pointing into Payment and Shipping, either aggregated by a gateway/BFF or emitted directly from each service.

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.

14

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

!
“Fake HATEOAS” · the most common anti‑pattern

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.

15

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 _links shape.
  • 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 rel names 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:

QuestionLeans toward HATEOASLeans toward plain REST (Level 2)
How many independent client teams consume this API?Many, including third parties you don’t controlOne 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 clientsRarely, 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 acceptableYes, 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.

16

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

StyleHow clients learn what’s possibleTypical use case
RPC / Level 0‑1 RESTStatic documentation, read once at development timeSimple internal services, single client team
Level 2 REST (most “REST APIs”)OpenAPI/Swagger spec + fixed endpoint documentationPublic developer‑facing APIs with SDKs (Stripe, Twilio)
HATEOAS / Level 3 RESTRuntime links in each response, discovered dynamicallyLong‑lived multi‑step workflows, payments, multi‑party integrations
GraphQLIntrospectable schema, client asks for exact fields neededAggregating many backend sources for flexible frontend needs
gRPCCompiled Protobuf contract, generated client stubsHigh‑performance internal service‑to‑service calls
17

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.

18

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.
The one‑sentence version

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.

Retrofit hypermedia onto an API that already has thousands of clients hard‑coding URLs and you’re paying twice — design it in from day one, or make peace with Level 2 as a deliberate choice, not an accident.

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.