HATEOAS: Pragmatic REST and When Hypermedia Really Makes Sense
AI generated
{ }
GET
REST API · HATEOAS · Hypermedia · API Design
HATEOAS: Pragmatic REST
and when hypermedia really makes sense

HATEOAS is the principle that separates REST from a simple HTTP data transport, and at the same time the most debated and least implemented REST constraint. This article explains what hypermedia actually means, which formats have proven themselves, and when pragmatism should win over purity.

12 min read HAL · JSON:API · Richardson Maturity Model · REST Level 3 HTTP · REST · API Design

1. What HATEOAS Actually Means

HATEOAS stands for Hypermedia as the Engine of Application State and is one of the six constraints Roy Fielding defined for REST in his 2000 dissertation. The core of the principle is simple: an API client should never need to know URLs in advance to interact with an API. Instead, every API response contains links to the next possible actions, much like a web browser navigates pages without needing to know every link's URL beforehand.

In practice this means: when a client fetches an article, the response contains not only the article data, but also links such as self (the URL of the article itself), edit (the URL for editing), delete and author. The client decides which action to take next based on these links, without having a single URL other than the API entry point hardcoded in its code. This principle theoretically makes APIs versioning-independent: if the server changes the URLs, all clients simply follow the new links.

Why is HATEOAS so rarely implemented in full? Because it involves considerable effort on both the client and server side, while offering real value only in certain scenarios. Most APIs serve a known, tightly coupled client. In that case, full HATEOAS is often over-engineering, but understanding the principle still helps produce better API designs.

2. The Richardson Maturity Model, Level 0 to 3

Leonard Richardson described a maturity model for REST APIs in 2008 that distinguishes four levels. Level 0 is the RPC-over-HTTP approach: a single URL, all requests via POST, and a self-defined XML or JSON protocol. SOAP web services and many older JSON-RPC APIs fall into this category. HTTP here is merely a transport protocol, not a semantic foundation.

Level 1 introduces resources: instead of a single URL, there are separate URLs for each resource type (/orders, /customers, /products). Level 2 uses HTTP verbs correctly: GET for reads, POST for creation, PUT/PATCH for updates, DELETE for deletion. HTTP status codes are used with the correct semantics: 200, 201, 204, 404, 409, 422. Most production REST APIs that deserve the name sit at Level 2.

Level 3 is HATEOAS: every response contains hypermedia links to the next possible actions. The API client only needs to know the entry point and navigates from there through every state. The model is a useful classification, not a quality judgment: a Level 2 API can be excellently designed, and a Level 3 API can be poorly designed. The maturity model describes properties, not merit.


// Level 2: Plain REST, client must know /orders/{id}/cancel
{
  "id": 42,
  "status": "pending",
  "total": 129.90,
  "customerId": 7
}

// Level 3: HATEOAS, client just follows the link
{
  "id": 42,
  "status": "pending",
  "total": 129.90,
  "_links": {
    "self":   { "href": "/orders/42" },
    "cancel": { "href": "/orders/42/cancel", "method": "POST" },
    "pay":    { "href": "/orders/42/payment", "method": "POST" },
    "customer": { "href": "/customers/7" }
  }
}

3. HAL: Hypertext Application Language in Practice

HAL (Hypertext Application Language) is the best-known hypermedia format for JSON APIs. It defines two reserved fields: _links for links to related resources and actions, and _embedded for embedded resources that are fully included in the response to avoid roundtrips. HAL has the media type application/hal+json and is specified as an internet draft at the IETF, even though the draft never became an RFC.

The strength of HAL lies in its simplicity. A _links object contains named link relations as keys, where each relation is either a link object with href or an array of link objects. Optional fields such as title, type (media type of the linked resource), and templated (for URI templates like /orders{?page,size}) round out the base format. The _embedded field allows a list of orders to be embedded directly in the response instead of forcing an additional GET request per order.


// HAL response: order list with embedded resources
{
  "_links": {
    "self":  { "href": "/orders?page=1&size=10" },
    "next":  { "href": "/orders?page=2&size=10" },
    "first": { "href": "/orders?page=1&size=10" },
    "last":  { "href": "/orders?page=5&size=10" },
    "create": { "href": "/orders", "method": "POST", "title": "Create a new order" }
  },
  "totalCount": 47,
  "page": 1,
  "size": 10,
  "_embedded": {
    "orders": [
      {
        "id": 42,
        "status": "pending",
        "total": 129.90,
        "_links": {
          "self":   { "href": "/orders/42" },
          "cancel": { "href": "/orders/42/cancel", "method": "POST" },
          "customer": { "href": "/customers/7" }
        }
      }
    ]
  }
}

4. JSON:API as a Hypermedia Alternative

JSON:API (jsonapi.org) is a complete specification for JSON-based APIs that goes beyond simple hypermedia links. JSON:API defines a standardized format for resources, relationships, errors, pagination and links. Its media type is application/vnd.api+json. Every resource has a unique id and a type, attributes are bundled in an attributes object, and relationships live in a relationships object.

JSON:API contains standardized link fields at both the document and resource level. A document can contain links.self, links.first, links.prev, links.next and links.last for pagination. Resources can contain individual links to themselves and to related resources. The advantage over HAL: JSON:API is far more extensively specified, which leads to greater interoperability between client libraries. The downside: the format is more verbose and requires more effort to understand.

The choice between HAL and JSON:API mostly depends on the client landscape. If several independent clients consume the API and client libraries are expected to be used, JSON:API is more attractive thanks to its broader ecosystem support. For internal APIs with a single known client, HAL is often the better choice due to its simplicity.

The heart of HATEOAS is not the format, but the semantics of link relations. The key in a _links object, for example self, next, author or cancel, is a link relation (short: rel). IANA maintains an official registry of standardized link relations: self for the canonical URL of the resource, next and prev for pagination, alternate for alternative representations, edit for the edit endpoint.

For application-specific relations not found in the IANA registry, there are two approaches. The RFC-compliant path is an absolute URI as the relation: "https://api.mironsoft.de/rels/cancel-order". In practice, short, self-explanatory identifiers like cancel, approve or ship are usually used and described in the API documentation. That is more pragmatic, but less interoperable. Anyone implementing HATEOAS consistently documents all link relations in an API profile or refers to IANA-registered relations wherever possible.

6. Client-Driven Navigation Instead of Hardcoded URLs

The real promise of HATEOAS is that the API client needs to know no URLs other than the entry point. In practice this means the client sends a request against the API root (GET /api/) at startup and extracts the URLs for all further actions from that response. This entry point is the only contract between client and server; every other URL can change without breaking the client.

What does that look like concretely? An order-process client calls GET /api/, finds _links.orders.href in it, and then performs GET /orders. In that response it finds _links.create for new orders. After creating an order, the 201 response contains _links.pay and _links.cancel. The client never knows how /orders/42/payment is constructed; it simply follows the link. This pattern has been standard in web browsers for decades; for API clients it requires a rethink in architecture.


# HATEOAS navigation: only the entry point is hardcoded
BASE="https://api.mironsoft.de/api"

# Step 1: fetch the entry point
ROOT=$(curl -s -H "Accept: application/hal+json" "$BASE/")
ORDERS_URL=$(echo "$ROOT" | jq -r '._links.orders.href')

# Step 2: load orders (URL from link relation, not hardcoded)
ORDERS=$(curl -s -H "Accept: application/hal+json" "$ORDERS_URL")
CREATE_URL=$(echo "$ORDERS" | jq -r '._links.create.href')

# Step 3: create a new order
NEW_ORDER=$(curl -s -X POST "$CREATE_URL" \
  -H "Content-Type: application/json" \
  -H "Accept: application/hal+json" \
  -d '{"productId": 15, "quantity": 2}')

# Step 4: extract the payment URL from the response, no hardcoded URL
PAY_URL=$(echo "$NEW_ORDER" | jq -r '._links.pay.href')
curl -s -X POST "$PAY_URL" -H "Content-Type: application/json" \
  -d '{"method": "credit_card", "token": "tok_xyz"}'

7. When HATEOAS Really Makes Sense

HATEOAS unfolds its value in very specific scenarios. The first is the public API with unknown clients: when an API is consumed by third-party developers who evolve their clients independently, hypermedia links significantly reduce coupling to concrete URL structures. If the server changes URL patterns, for example when introducing API versioning or restructuring resource hierarchies, clients that have consistently implemented HATEOAS remain functional.

The second scenario is state-dependent workflows, where certain actions are only available in certain states. An order can only be paid if it is in the pending status. It can only be canceled if it has not yet shipped. In a HATEOAS API, the absence of the corresponding link directly signals that the action is unavailable, with no client-side state management and no hardcoded business rules in the frontend. The cancel link only appears if the server actually allows cancellation.

HATEOAS, on the other hand, is not worthwhile for tightly coupled internal APIs consumed by a single known client. If frontend and backend live in the same repository and are deployed together, HATEOAS brings no decoupling benefit but considerable implementation effort. For mobile apps that update rarely and are bound by app store processes, HATEOAS can solve versioning problems, but only if the client framework actually navigates in a link-driven way.

8. The Pragmatic Middle Ground

The pragmatic recommendation for most projects: partial hypermedia. That means adding links selectively where they offer real value, without fully implementing HATEOAS. Concretely: a self link in every resource is always worthwhile and costs almost nothing. Pagination links (next, prev, first, last) are a universally useful pattern that simplifies client implementation. State-dependent action links for workflows make sense wherever the allowed actions change depending on resource state.

What partial hypermedia does not mean: adding URLs for every conceivable resource. A user object does not need a link to all of its orders if the client never navigates there dynamically anyway and instead always calls /orders?userId=7 directly. The middle ground requires deliberate decisions: where does the client actually navigate dynamically? Where are the links just formal completeness without practical benefit? That trade-off matters more than dogmatically sticking to Level 3.

9. HATEOAS Formats Compared

Criterion Plain JSON HAL JSON:API
Entry barrier Very low Low Medium to high
Standardization None Internet draft (IETF) Full specification
Client libraries None Few Extensive ecosystem
Pagination Define yourself Links convention Standardized
Response size Small Medium Large
State-dependent links Not supported Possible Possible

The choice of format is less important than consistency in implementation. An inconsistent HATEOAS implementation that only delivers links on some endpoints offers neither the benefits of the partial hypermedia approach nor full decoupling. Anyone who chooses HAL or JSON:API should apply it consistently across all endpoints and clearly describe the link relations in the API documentation.

Mironsoft

REST API design, hypermedia architecture and API documentation

REST APIs that are actually RESTful?

We design and implement REST APIs that use HATEOAS pragmatically, with clear link relations, consistent hypermedia patterns and API documentation that genuinely helps clients.

API design review

Analysis of existing APIs for REST conformance, hypermedia potential and consistency

HAL / JSON:API

Implementing hypermedia links in existing REST APIs, incrementally and backward-compatibly

OpenAPI documentation

Complete API specification with link relations, state diagrams and example flows

10. Summary

HATEOAS is not an end in itself, but a tool for the right situation. The Richardson Maturity Model helps with classification, but it is not a certification program. Level 2, correct HTTP verbs, status codes and resource-oriented URLs, is the solid foundation on which the vast majority of production APIs should stand. Level 3 with full HATEOAS pays off when real decoupling benefits emerge: for public APIs with independent clients, for state-dependent workflows, and for APIs that must be versioned over long periods without breaking clients.

The pragmatic middle ground, pagination links, self links and state-dependent action links, offers the greatest benefit per unit of implementation effort. HAL is the right format for most projects: simple enough to consume without special libraries, and standardized enough to be interoperable. JSON:API is the better choice when a strong ecosystem with client libraries is to be used and the more extensive format is not a problem.

HATEOAS and Hypermedia, The Essentials at a Glance

Richardson Maturity Model

Level 0 (RPC) → Level 1 (resources) → Level 2 (HTTP verbs) → Level 3 (HATEOAS). Most APIs should aim for Level 2, Level 3 only where real decoupling is needed.

Link relations

Use IANA-standardized relations such as self, next, prev wherever possible. Describe custom relations in the API documentation. Consistency matters more than completeness.

HAL vs. JSON:API

HAL is simpler and well suited to internal APIs. JSON:API is more fully specified and has more client libraries, better for public APIs with a broad client landscape.

Pragmatic starting point

Start with self links and pagination links. Add state-dependent action links for workflows. Full HATEOAS navigation only where real decoupling is needed.

11. FAQ: HATEOAS and Hypermedia in REST APIs

1What is HATEOAS?
Hypermedia as the Engine of Application State, API responses contain links to the next actions. The client only knows the entry point and navigates dynamically via links.
2Richardson Maturity Model?
Level 0 (RPC) → Level 1 (resources) → Level 2 (HTTP verbs) → Level 3 (HATEOAS). Describes properties, not a quality judgment. Level 2 is the solid basis for most APIs.
3HAL vs. JSON:API?
HAL simpler, specified as an IETF draft. JSON:API more complete, more client libraries. HAL for internal APIs, JSON:API for public APIs with a broad ecosystem.
4When to use HATEOAS?
For public APIs with unknown clients and for state-dependent workflows. Not for tightly coupled internal APIs with a single known client.
5What are link relations?
Keys in _links objects (self, next, cancel etc.). Prefer IANA-standardized relations. Describe custom relations in the API documentation.
6How does a HATEOAS client navigate?
Only the entry point is known. Links are extracted from every response. No further URLs are hardcoded. URL changes on the server do not break clients.
7What is partial hypermedia?
Pragmatic middle ground: self links, pagination links and state-dependent action links, without implementing full HATEOAS navigation.
8Signaling unavailable actions?
Through the absence of the link. No cancel link means cancellation is not possible. The client implements no state machine of its own; the server communicates state via links.
9Is Level 2 worse than Level 3?
No. The maturity model describes properties. An excellent Level 2 API is better than a poorly implemented Level 3 API. HATEOAS only where it makes sense.
10What is a URI template in HAL?
A link with templated: true per RFC 6570, e.g. /orders{?page,size}. The client expands it with concrete values. Useful for filterable, searchable resources.