REST API HATEOAS and Hypermedia APIs

You use hypermedia every time you browse a website. You open a homepage, click a link, land on a product page, click "Add to Cart," then click "Checkout." You never needed to memorize those URLs ahead of time. The page itself told you what to do next.

HATEOAS brings this same idea to REST APIs. Instead of requiring client developers to memorize every possible endpoint, the API response includes links that tell the client what actions are available right now, based on the current state of the resource.

HATEOAS stands for Hypermedia As The Engine Of Application State. Roy Fielding — the person who defined REST — included HATEOAS as one of REST's core constraints. Most APIs ignore it. The ones that implement it become far easier to evolve and maintain over time.

The Problem HATEOAS Solves

A Non-HATEOAS API

  Client calls: GET /orders/1001

  Response:
  {
    "orderId": 1001,
    "status": "pending",
    "total": 59.99
  }

  The client developer must now:
  1. Read the full API documentation
  2. Memorize that pending orders can be cancelled at DELETE /orders/1001
  3. Memorize that payment is submitted at POST /orders/1001/pay
  4. Know that these URLs might change in future API versions
  5. Write code that hardcodes these URL patterns

  Problem: If the API changes URLs, ALL clients break.
           Clients are tightly coupled to URL structure.

The Same API with HATEOAS

  Client calls: GET /orders/1001

  Response:
  {
    "orderId": 1001,
    "status": "pending",
    "total": 59.99,
    "_links": {
      "self":   { "href": "/orders/1001",         "method": "GET" },
      "pay":    { "href": "/orders/1001/pay",      "method": "POST" },
      "cancel": { "href": "/orders/1001/cancel",   "method": "DELETE" },
      "items":  { "href": "/orders/1001/items",    "method": "GET" }
    }
  }

  The client developer:
  1. Reads the response
  2. Sees available actions in "_links"
  3. Follows links by their name ("pay", "cancel")
  4. Never hardcodes URLs

  Benefit: API can change URLs freely.
           Client follows links, not memorized paths.

How HATEOAS Works — The ATM Analogy

Think of an ATM. When you insert your card, the screen shows options: Enter PIN, Cancel. After you enter your PIN, new options appear: Check Balance, Withdraw, Deposit, Transfer. The ATM controls what you can do at each step. It never shows "Transfer" before you log in. Each screen is a state. Each button is a link to the next state.

  ATM State Machine → Same as HATEOAS

  STATE: Card Inserted
  Available actions: [enter_pin, eject_card]

  STATE: Authenticated
  Available actions: [check_balance, withdraw, deposit, transfer, logout]

  STATE: Withdrawal Selected
  Available actions: [enter_amount, cancel]

  STATE: Amount Confirmed
  Available actions: [collect_cash, print_receipt]

  HATEOAS API does exactly this — each response tells you
  what you can do next, based on the current state.

State-Based Links — Order Lifecycle Example

A key feature of HATEOAS is that links change based on the resource's current state. A cancelled order should not offer a "cancel" link. A delivered order should not offer a "pay" link. The API enforces valid transitions, just like the ATM.

  ORDER STATE: pending
  {
    "orderId": 1001,
    "status": "pending",
    "_links": {
      "self":    { "href": "/orders/1001" },
      "pay":     { "href": "/orders/1001/pay",    "method": "POST" },
      "cancel":  { "href": "/orders/1001/cancel", "method": "DELETE" }
    }
  }

  ORDER STATE: paid (after payment)
  {
    "orderId": 1001,
    "status": "paid",
    "_links": {
      "self":    { "href": "/orders/1001" },
      "ship":    { "href": "/orders/1001/ship",   "method": "POST" },
      "refund":  { "href": "/orders/1001/refund", "method": "POST" }
      // "pay" is gone — order is already paid
      // "cancel" is gone — paid orders follow refund process
    }
  }

  ORDER STATE: shipped
  {
    "orderId": 1001,
    "status": "shipped",
    "_links": {
      "self":    { "href": "/orders/1001" },
      "track":   { "href": "/orders/1001/tracking" },
      "deliver": { "href": "/orders/1001/deliver", "method": "POST" }
      // "refund" gone — must wait for delivery before return
    }
  }

  ORDER STATE: delivered
  {
    "orderId": 1001,
    "status": "delivered",
    "_links": {
      "self":    { "href": "/orders/1001" },
      "review":  { "href": "/orders/1001/review",  "method": "POST" },
      "return":  { "href": "/orders/1001/return",  "method": "POST" }
    }
  }

Link Formats — How to Structure Hypermedia

There is no single mandatory format for HATEOAS links, but two widely-used standards make implementation and consumption consistent: HAL and JSON:API.

HAL — Hypertext Application Language

HAL is the most popular HATEOAS format. It uses _links for related links and _embedded for nested resources.

  HAL Response for GET /users/42:
  {
    "id": 42,
    "name": "Alice",
    "email": "alice@example.com",
    "_links": {
      "self":    { "href": "/users/42" },
      "orders":  { "href": "/users/42/orders" },
      "profile": { "href": "/users/42/profile" },
      "edit":    { "href": "/users/42", "method": "PUT" },
      "delete":  { "href": "/users/42", "method": "DELETE" }
    },
    "_embedded": {
      "latestOrder": {
        "orderId": 1001,
        "status": "pending",
        "_links": {
          "self": { "href": "/orders/1001" }
        }
      }
    }
  }

  HAL Content-Type: application/hal+json

JSON:API Format

  JSON:API Response for GET /articles/1:
  {
    "data": {
      "type": "articles",
      "id": "1",
      "attributes": {
        "title": "HATEOAS Explained",
        "content": "..."
      },
      "relationships": {
        "author": {
          "links": {
            "self":    "/articles/1/relationships/author",
            "related": "/articles/1/author"
          }
        }
      }
    },
    "links": {
      "self": "/articles/1"
    }
  }

  JSON:API Content-Type: application/vnd.api+json

Link Relations — The Language of Hypermedia

Each link has a relation type (rel) that describes the meaning of the link. IANA maintains a registry of standard relation types that all hypermedia APIs can use. Using standard relations makes your API understandable to generic clients.

  STANDARD IANA LINK RELATIONS:

  "self"         → The link to this exact resource
  "next"         → Next page in a paginated list
  "prev"         → Previous page in a paginated list
  "first"        → First page of results
  "last"         → Last page of results
  "collection"   → The collection this item belongs to
  "item"         → An item within this collection
  "related"      → A related resource
  "edit"         → Link to update this resource
  "delete"       → Link to delete this resource
  "search"       → Link to search the collection

  CUSTOM RELATIONS (for domain-specific actions):
  "pay"          → Custom: submit payment for this order
  "cancel"       → Custom: cancel this order
  "ship"         → Custom: mark order as shipped
  "approve"      → Custom: approve a pending item

  Custom relations should use a full URI as the rel:
  "rel": "https://api.yourstore.com/rels/pay"

Pagination with HATEOAS

HATEOAS makes pagination trivial for clients. Instead of calculating page offsets, the client follows "next" and "prev" links. The server handles the math.

  GET /products?page=2

  Response:
  {
    "page": 2,
    "totalPages": 10,
    "totalItems": 95,
    "items": [
      { "id": 11, "name": "Laptop" },
      { "id": 12, "name": "Mouse" },
      ...
    ],
    "_links": {
      "self":  { "href": "/products?page=2" },
      "first": { "href": "/products?page=1" },
      "prev":  { "href": "/products?page=1" },
      "next":  { "href": "/products?page=3" },
      "last":  { "href": "/products?page=10" }
    }
  }

  Client code becomes simple:
  while (response._links.next) {
    response = fetch(response._links.next.href);
    processItems(response.items);
  }

  The client never builds URLs. It just follows links.

HATEOAS and API Versioning

One of HATEOAS's biggest practical benefits is that it decouples clients from URL structure. When you restructure your API, clients that follow links adapt automatically.

  VERSION 1 of your API:
  "_links": {
    "pay": { "href": "/orders/1001/payment", "method": "POST" }
  }

  VERSION 2 — you renamed the endpoint:
  "_links": {
    "pay": { "href": "/v2/orders/1001/transactions", "method": "POST" }
  }

  A HATEOAS client: Follows the "pay" link wherever it points.
                    Works with both versions without code changes.

  A hard-coded client: POST /orders/{id}/payment hardcoded.
                       Breaks when v2 renames the endpoint.

Implementing HATEOAS — Practical Guide

Step 1: Define Your Resource States

  For each resource, list all possible states:

  Order: pending → paid → processing → shipped → delivered
                       → cancelled
                       → refunded

  Draw the state diagram before writing code.
  Each state has a different set of valid transitions.

Step 2: Map Transitions to Links

  Order State | Available Transitions | Links to Include
  ────────────┼───────────────────────┼─────────────────────────
  pending     | pay, cancel           | pay, cancel, self
  paid        | ship, refund          | ship, refund, self
  processing  | ship                  | ship, self
  shipped     | deliver               | deliver, track, self
  delivered   | return, review        | return, review, self
  cancelled   | (none)                | self only
  refunded    | (none)                | self only

Step 3: Build Links Dynamically in Code

  // Pseudocode: Link builder for Order resource
  function buildOrderLinks(order) {
    const links = {
      self: { href: `/orders/${order.id}`, method: "GET" }
    };

    if (order.status === "pending") {
      links.pay    = { href: `/orders/${order.id}/pay`,    method: "POST" };
      links.cancel = { href: `/orders/${order.id}/cancel`, method: "DELETE" };
    }

    if (order.status === "paid") {
      links.ship   = { href: `/orders/${order.id}/ship`,   method: "POST" };
      links.refund = { href: `/orders/${order.id}/refund`, method: "POST" };
    }

    if (order.status === "shipped") {
      links.deliver = { href: `/orders/${order.id}/deliver`, method: "POST" };
      links.track   = { href: `/orders/${order.id}/tracking` };
    }

    return links;
  }

When HATEOAS Makes Sense (and When It Doesn't)

  HATEOAS IS VALUABLE WHEN:

  ✓ Your API has complex workflows with many states
  ✓ Multiple clients (mobile, web, third-party) consume the API
  ✓ The API will evolve over time with URL changes
  ✓ You want to prevent clients from building invalid state transitions
  ✓ You want self-documenting, explorable APIs

  HATEOAS MAY ADD UNNECESSARY COMPLEXITY WHEN:

  ✗ You control both the API and the only client that uses it
  ✗ The API is simple CRUD with no complex state transitions
  ✗ Your team is small and fast iteration is the priority
  ✗ Consumers are non-technical and cannot use hypermedia clients
  ✗ Response payload size is critical (links add bytes)

Richardson Maturity Model — Where HATEOAS Fits

The Richardson Maturity Model describes four levels of REST maturity. HATEOAS is the highest level, called "Glory of REST."

  LEVEL 0 — The Swamp of POX
  One endpoint, all operations use POST
  POST /api  { "action": "getUser", "id": 123 }

  LEVEL 1 — Resources
  Separate URLs for each resource
  GET /users/123
  GET /orders/456

  LEVEL 2 — HTTP Verbs
  Use correct HTTP methods with resources
  GET /users/123
  PUT /users/123
  DELETE /users/123

  LEVEL 3 — Hypermedia Controls (HATEOAS)
  Responses include links to next actions
  GET /users/123
  {
    "id": 123,
    "_links": {
      "orders": { "href": "/users/123/orders" },
      "edit":   { "href": "/users/123", "method": "PUT" }
    }
  }

  Most production APIs operate at Level 2.
  HATEOAS (Level 3) is the true REST ideal.

Key Points

  • HATEOAS means the API response includes links to valid next actions. Clients follow links instead of memorizing URL structures.
  • Links change based on the resource's current state. A cancelled order shows no "pay" link. A delivered order shows no "ship" link. The API enforces valid state transitions.
  • HAL (application/hal+json) is the most common HATEOAS format. It uses _links for links and _embedded for nested resources.
  • Pagination with HATEOAS is clean: include next, prev, first, last links so clients never calculate offsets.
  • HATEOAS decouples clients from URL structure — when you rename an endpoint, HATEOAS-aware clients adapt automatically.
  • HATEOAS adds the most value when workflows are complex and multiple client types consume the same API over a long period.

Leave a Comment