API Design

HATEOAS

Hypermedia as the Engine of Application State — the highest level of REST maturity, where API responses include links to valid next actions, decoupling clients from hardcoded URL templates and enabling server-driven navigation through the _links object, HAL, JSON:API, and Siren formats.

⏱ 11 min read

What it is

HATEOAS (Hypermedia as the Engine of Application State) is a constraint of the REST architectural style defined by Roy Fielding in his 2000 dissertation. It states that a client should be able to interact with a REST API entirely through hypermedia links provided dynamically by the server — the client needs to know only the API's entry point URL and media type, not the URL structure.

In practice, HATEOAS means including a _links (or equivalent) object in every response that describes the available transitions from the current state. A client navigates the API by following links rather than constructing URLs from out-of-band knowledge (documentation, hardcoded templates).

Richardson's Maturity Model defines HATEOAS as Level 3 — the highest level of REST maturity, beyond basic HTTP verbs (Level 1) and resource modeling (Level 2).

Why it exists

Without hypermedia, REST clients are coupled to URL structures. When the server changes /users/{id}/orders to /accounts/{id}/orders, every client breaks. With HATEOAS, the server advertises the URL in the response — clients follow links, not hardcoded paths. The server controls navigation, can change URLs freely, and can conditionally include or exclude links based on state (e.g., a "cancel" link is only present when the order is cancellable).

When to use

  • Long-lived public APIs where clients are deployed independently and can't be updated in lockstep.
  • Machine-readable APIs designed for generic crawlers or workflow automation tools.
  • APIs where state-dependent actions (e.g., "approve", "cancel", "reopen") need to be surfaced to the client only when valid.
  • APIs explicitly designed for use by unknown third-party developers over a long horizon.

When not to use

  • When clients are internal and deployed with the server — tight coupling is acceptable and the added complexity isn't worth it.
  • When consumers are mobile apps or SPAs with typed client SDKs — they won't traverse links dynamically anyway.
  • High-performance APIs where extra JSON payload size and link traversal round-trips matter.

Hypermedia formats

HAL (Hypertext Application Language)

GET /orders/42
{
  "id": "42",
  "status": "confirmed",
  "total": 149.99,
  "_links": {
    "self":    { "href": "/orders/42" },
    "cancel":  { "href": "/orders/42/cancellations", "method": "POST" },
    "invoice": { "href": "/orders/42/invoice" },
    "customer":{ "href": "/customers/7" }
  },
  "_embedded": {
    "items": [
      {
        "productId": "sku-101",
        "quantity": 2,
        "_links": {
          "product": { "href": "/products/sku-101" }
        }
      }
    ]
  }
}

HAL (application/hal+json) adds _links (navigation) and _embedded (pre-fetched related resources) to a plain JSON object. It's the most widely adopted HATEOAS format.

JSON:API

GET /articles/1
{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": { "title": "Building APIs", "body": "..." },
    "relationships": {
      "author": {
        "links": { "related": "/articles/1/author" },
        "data": { "type": "people", "id": "9" }
      }
    },
    "links": { "self": "/articles/1" }
  },
  "included": [
    { "type": "people", "id": "9", "attributes": { "name": "Jane" } }
  ]
}

JSON:API (application/vnd.api+json) is more opinionated: it standardises resource structure, relationships, compound documents (included), filtering, sorting, and pagination query parameters. Good for document-oriented APIs with rich relationships.

Siren

{
  "class": ["order"],
  "properties": { "id": "42", "status": "confirmed" },
  "entities": [
    {
      "rel": ["items"],
      "href": "/orders/42/items"
    }
  ],
  "actions": [
    {
      "name": "cancel-order",
      "href": "/orders/42/cancellations",
      "method": "POST",
      "fields": [{ "name": "reason", "type": "text" }]
    }
  ],
  "links": [
    { "rel": ["self"], "href": "/orders/42" }
  ]
}

Siren adds actions — structured form-like descriptions of non-safe transitions including the method, fields, and encoding type. More expressive than HAL for APIs with complex write operations.

Pros and cons

Pros

  • Server can change URLs without breaking clients that follow links.
  • State-dependent links prevent clients from attempting invalid transitions (no "cancel" link = order can't be cancelled).
  • Reduces out-of-band coupling — clients don't need to hardcode URL templates or read API documentation to navigate.
  • Discoverability: a generic client can explore the API from the entry point without prior knowledge.

Cons

  • Most REST clients (typed SDKs, OpenAPI generated code) ignore hypermedia links anyway — the benefit is theoretical in practice.
  • Extra payload overhead — every response carries link metadata; significant at high volume.
  • Complex to implement correctly — link generation requires the server to know its own base URL and all valid transitions per resource state.
  • No single dominant standard — HAL, JSON:API, Siren, Collection+JSON all compete; ecosystem fragmentation.
  • GraphQL solves discoverability differently (introspection) and has broader adoption.

Implementation notes

Practical minimum: always include self links

Even if full HATEOAS isn't implemented, every resource response should include a self link pointing to its canonical URL. This alone removes a class of client bugs where the client has to reconstruct the URL from a returned ID.

State-conditional links

Only include action links that are valid in the current state. An order with status delivered should not have a cancel link. This allows clients to conditionally render UI actions (a "Cancel" button) based on the presence of the link rather than duplicating state machine logic client-side. Document the state machine in the API docs so developers know what states produce what links.

Link relations

Use standardised IANA link relation types where possible: self, next, prev, first, last (for pagination), related, collection, item. For domain-specific relations, use a URI (e.g., "https://api.example.com/rels/cancel") per RFC 8288 (Web Linking).

Pagination with Link header

For collection endpoints, the HTTP Link header (RFC 5988/8288) is the HTTP-native way to express pagination links without changing the JSON body. GitHub's API uses this pattern extensively:

Link: <https://api.github.com/repos/octocat/hello-world/commits?page=2>; rel="next",
      <https://api.github.com/repos/octocat/hello-world/commits?page=34>; rel="last"

Why most REST APIs skip HATEOAS

Despite being a formal REST constraint, HATEOAS is almost universally skipped by production APIs, including the most well-designed ones (Stripe, Twilio, GitHub). The reasons are pragmatic: (1) clients are almost always typed-SDK consumers that don't dynamically follow links; (2) OpenAPI documentation provides the discoverability that hypermedia was meant to enable; (3) the implementation overhead is real while the practical benefit for known consumers is marginal. HATEOAS is best understood as a design principle (APIs should be self-describing) rather than a mandatory implementation requirement.

Further reading

  • Roy Fielding's dissertation, Chapter 5 — Representational State Transfer (REST).
  • RFC 8288 — Web Linking (link relations and Link header).
  • HAL Specification — stateless.co/hal_specification.html
  • JSON:API Specification — jsonapi.org/format
  • Richardson Maturity Model — martinfowler.com/articles/richardsonMaturityModel.html