Skip to content
C Codeloom
REST APIs

REST API HATEOAS Explained

Understand Hypermedia as the Engine of Application State, why most REST APIs skip it, and when adding hypermedia links actually pays off.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What HATEOAS actually means
  • How hypermedia links drive state
  • HAL and JSON:API formats
  • When HATEOAS adds real value
  • Why most APIs skip it

Prerequisites

  • Familiar with HTTP

What and Why

HATEOAS, short for Hypermedia as the Engine of Application State, is the part of REST that most APIs quietly ignore. It says a client should not need out-of-band knowledge of your URLs. Instead, every response includes links describing the next legal actions, and the client navigates them like a browser navigates HTML.

Roy Fielding’s original constraint argued that this is what makes an API truly RESTful. In practice, most APIs are HTTP-based RPC. HATEOAS still matters in specific cases: long-lived integrations, workflow-driven APIs, and systems where state transitions change often.

Mental Model

Think of a web browser. You do not memorize URLs. You click links and submit forms. The server tells you what is possible next by what it renders. HATEOAS applies that idea to JSON APIs: each resource returns its own state plus links to related resources and allowed transitions.

GET /orders/42
{
"id": 42,
"status": "pending",
"_links": {
  "self":   { "href": "/orders/42" },
  "pay":    { "href": "/orders/42/pay"    },
  "cancel": { "href": "/orders/42/cancel" }
}
}
     |
     v  client POSTs to /orders/42/pay
+---------------------+
| status: "paid"      |
| _links: ship, refund|
+---------------------+
Hypermedia-driven state transitions

The set of links changes as state changes. A paid order no longer exposes pay; instead it exposes ship or refund.

Hands-on Example

The two most common hypermedia formats are HAL and JSON:API.

HAL is minimal. Add _links and optionally _embedded:

{
  "id": 42,
  "total": 1999,
  "status": "pending",
  "_links": {
    "self":   { "href": "/orders/42" },
    "pay":    { "href": "/orders/42/pay" },
    "cancel": { "href": "/orders/42/cancel" }
  },
  "_embedded": {
    "customer": {
      "id": 7,
      "name": "Ada Lovelace",
      "_links": { "self": { "href": "/customers/7" } }
    }
  }
}

JSON:API is stricter. It mandates a data envelope, typed resources, and a relationships block:

{
  "data": {
    "type": "orders",
    "id": "42",
    "attributes": { "total": 1999, "status": "pending" },
    "relationships": {
      "customer": {
        "data": { "type": "customers", "id": "7" },
        "links": { "related": "/customers/7" }
      }
    },
    "links": {
      "self":   "/orders/42",
      "pay":    "/orders/42/pay",
      "cancel": "/orders/42/cancel"
    }
  }
}

A HATEOAS-aware client checks for the pay link instead of hardcoding the URL. If the server later moves it to /orders/42/checkout, the client keeps working because it follows the link by relation name, not by path.

For collections, include pagination links: first, prev, next, last. This is the most adopted form of HATEOAS in the wild because it solves a concrete problem.

Common Pitfalls

Adding links nobody follows. If every client hardcodes URLs anyway, you have added bytes and complexity for zero benefit. HATEOAS pays off only when clients actually use links.

Inventing your own link format. Pick HAL, JSON:API, or Siren. Custom shapes mean every client writes a custom parser.

Treating links as documentation. Links are for runtime navigation, not for replacing your API reference. You still need human docs that explain what pay does.

Coupling link rels to URLs. A link relation like pay is a contract. The URL behind it can change. Do not let clients break when you reorganize routes.

Forgetting state. The whole point is that links reflect current state. If you always emit every link regardless of status, you have hypermedia in name only.

Practical Tips

  • Start small. Add self links and pagination links first. They are cheap and useful.
  • Document link relations in a rels/ directory or a Profile URI. Treat them as part of your API contract.
  • Use IANA-registered relations (self, next, prev, up) where they fit. Invent your own only when needed, and namespace them.
  • Pair HATEOAS with content negotiation. Serving application/hal+json or application/vnd.api+json lets clients opt in.
  • Consider it for partner integrations, where you cannot easily push SDK updates. Internal microservices rarely need it.
  • If using OpenAPI, you can describe links via the links field on responses. This gives you discoverability without going full hypermedia.

Wrap-up

HATEOAS is the most-cited and least-implemented REST constraint. It is powerful when clients are long-lived and state machines are complex, and overhead when they are not.

Be honest about your use case. Pagination links and self references are almost always worth it. Full state-driven hypermedia is a strategic choice, not a default. Pick a standard format, document your link relations, and add links only where they let clients do something they could not before.