Skip to content

Hypermedia APIs and HATEOAS — Complete Guide

DodaTech Updated 2026-06-23 10 min read

In this tutorial, you'll learn about Hypermedia APIs and HATEOAS. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

HATEOAS (Hypermedia as the Engine of Application State) is a constraint of REST architecture where API responses include hypermedia links that dynamically guide clients through available actions and related resources without prior knowledge of the API structure.

What You'll Learn

You will learn the HATEOAS constraint, hypermedia media types like HAL and Siren, implement dynamic link generation, build discoverable APIs, and understand when to apply hypermedia patterns.

Why HATEOAS Matters

HATEOAS is what separates truly RESTful APIs from HTTP APIs that merely use REST-like conventions. Without hypermedia, clients must hardcode URL structures, making them brittle when the API evolves. HATEOAS enables API evolution without breaking clients, reduces client-side logic, and makes APIs self-documenting.

Real-World Use

DodaTech applies hypermedia principles in specific scenarios. Doda Browser navigation API uses hypermedia links for dynamic menu generation, DodaZIP workflow API guides clients through multi-step file operations, and Durga Antivirus Pro investigation API uses links to define possible next actions based on threat State.

HATEOAS Learning Path

flowchart LR
  A[REST API Design] --> B[REST Constraints]
  B --> C[HATEOAS Concept]
  C --> D[Hypermedia Controls]
  D --> E[Media Types]
  E --> F[Implementation]
  F --> G[When to Use]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Prerequisites

Understand RESTful Api Design Best Practices thoroughly, including all six REST constraints. Familiarity with HTTP Protocol Basics and JSON Data Format is required.

What is HATEOAS?

HATEOAS stands for Hypermedia as the Engine of Application State. It is the fourth constraint of REST architecture (uniform interface). In a HATEOAS API, the server tells the client what actions are available next by including hypermedia links in responses.

Without HATEOAS, a client must know the URL structure beforehand:

Client: GET /api/users/42
Server: { "id": 42, "name": "Alice", "email": "alice@example.com" }
Client: (knows from documentation that /api/users/42/orders exists)
Client: GET /api/users/42/orders

With HATEOAS, the server provides the links:

Client: GET /api/users/42
Server: {
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "_links": {
    "self": { "href": "/api/users/42" },
    "orders": { "href": "/api/users/42/orders" },
    "update": { "href": "/api/users/42", "method": "PUT" },
    "delete": { "href": "/api/users/42", "method": "DELETE" }
  }
}
Client: (follows the "orders" link)
Client: GET /api/users/42/orders

Hypermedia Media Types

HAL (Hypertext Application Language)

HAL is the most popular hypermedia media type.

{
  "_links": {
    "self": { "href": "/API/users/42" },
    "orders": { "href": "/API/users/42/orders" },
    "update": { "href": "/API/users/42", "method": "PUT" },
    "delete": { "href": "/API/users/42", "method": "DELETE" },
    "curies": [
      {
        "name": "dt",
        "href": "HTTPS://docs.dodatech.com/rels/{rel}",
        "templated": true
      }
    ]
  },
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "admin",
  "_embedded": {
    "dt:orders": [
      {
        "_links": {
          "self": { "href": "/API/orders/100" }
        },
        "id": 100,
        "total": 250.00,
        "status": "shipped]
      }
    ]
  }
}

HAL elements:

  • _links — Hypermedia controls for navigation and actions
  • _embedded — Embedded related resources (no additional request needed)
  • curies — Compact URI definitions for documentation links

Siren

Siren is another hypermedia media type with additional action support.

{
  "class": ["user"],
  "properties": {
    "id": 42,
    "name": "Alice",
    "email": "alice@example.com",
    "role": "admin"
  },
  "actions": [
    {
      "name": "update-user",
      "title": "Update User",
      "method": "PUT",
      "href": "/API/users/42",
      "type": "application/JSON",
      "fields": [
        { "name": "name", "type": "text" },
        { "name": "email", "type": "email" }
      ]
    },
    {
      "name": "delete-user",
      "title": "Delete User",
      "method": "DELETE",
      "href": "/API/users/42"
    }
  ],
  "links": [
    { "rel": ["self"], "href": "/API/users/42" },
    { "rel": ["orders"], "href": "/API/users/42/orders" },
    { "rel": ["audit-log"], "href": "/API/users/42/audit" }
  ]
}

Siren elements:

  • class — Type classification for the entity
  • properties — Standard data fields
  • actions — Available State transitions with form descriptions
  • links — Navigation links

Structured Data with Links

A simpler custom approach for most APIs:

{
  "data": {
    "id": 42,
    "name": "Alice",
    "email": "alice@example.com",
    "role": "admin"
  },
  "links": {
    "self": { "href": "/API/v1/users/42", "method": "GET" },
    "update": { "href": "/API/v1/users/42", "method": "PUT", "title": "Update this user" },
    "delete": { "href": "/API/v1/users/42", "method": "DELETE", "title": "Delete this user" },
    "orders": { "href": "/API/v1/users/42/orders?page=1&limit=20", "method": "GET", "title": "View orders" },
    "collection": { "href": "/API/v1/users", "method": "GET", "title": "All users" }
  },
  "actions": [
    {
      "name": "change-role",
      "method": "PATCH",
      "href": "/API/v1/users/42/role",
      "fields": [
        { "name": "role", "type": "select", "options": ["admin", "member", "viewer"] }
      ]
    }
  ]
}

Implementing HATEOAS in Express.js

Step 1: Link Generator

// src/hypermedia/links.js
class LinkGenerator {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  userLinks(userId) {
    return {
      self: { href: `/API/v1/users/${userId}`, method: "GET" },
      update: { href: `/API/v1/users/${userId}`, method: "PUT" },
      delete: { href: `/API/v1/users/${userId}`, method: "DELETE" },
      orders: { href: `/API/v1/users/${userId}/orders`, method: "GET" },
      collection: { href: "/API/v1/users", method: "GET" }
    };
  }

  orderLinks(orderId) {
    return {
      self: { href: `/API/v1/orders/${orderId}`, method: "GET" },
      update: { href: `/API/v1/orders/${orderId}`, method: "PUT" },
      cancel: { href: `/API/v1/orders/${orderId}/cancel`, method: "POST" },
      items: { href: `/API/v1/orders/${orderId}/items`, method: "GET" },
      collection: { href: "/API/v1/orders", method: "GET" }
    };
  }

  paginationLinks(baseUrl, page, limit, total) {
    const totalPages = Math.ceil(total / limit);
    const links = {};

    if (page < totalPages) {
      links.next = { href: `${baseUrl}?page=${page + 1}&limit=${limit}`, method: "GET" };
    }
    if (page > 1) {
      links.prev = { href: `${baseUrl}?page=${page - 1}&limit=${limit}`, method: "GET" };
    }
    links.first = { href: `${baseUrl}?page=1&limit=${limit}`, method: "GET" };
    links.last = { href: `${baseUrl}?page=${totalPages}&limit=${limit}`, method: "GET" };

    return links;
  }
}

module.exports = LinkGenerator;

Step 2: Controller with Hypermedia

// src/controllers/userController.js
const LinkGenerator = require("../hypermedia/links");
const links = new LinkGenerator("HTTPS://API.dodatech.com/v1");

exports.getUser = async (req, res) => {
  const user = await userModel.findById(req.params.id);

  if (!user) {
    return res.status(404).JSON({
      error: "User not found",
      links: {
        collection: { href: "/API/v1/users", method: "GET" },
        create: { href: "/API/v1/users", method: "POST" }
      }
    });
  }

  const response = {
    data: user,
    links: links.userLinks(user.id)
  };

  // Only show admin links if current user is admin
  if (req.user.role !== "admin") {
    delete response.links.delete;
  }

  res.JSON(response);
};

exports.listUsers = async (req, res) => {
  const { page = 1, limit = 20 } = req.query;
  const offset = (page - 1) * limit;

  const users = await userModel.findAll(limit, offset);
  const total = await userModel.count();

  const response = {
    data: users.map(user => ({
      ...user,
      links: links.userLinks(user.id)
    })),
    links: {
      ...links.paginationLinks("/API/v1/users", page, limit, total),
      create: { href: "/API/v1/users", method: "POST" }
    },
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total: Number(total),
      pages: Math.ceil(total / limit)
    }
  };

  res.JSON(response);
};

exports.createUser = async (req, res) => {
  const user = await userModel.create(req.body);

  res.status(201).JSON({
    data: user,
    links: links.userLinks(user.id)
  });
};

State Machines with HATEOAS

HATEOAS becomes powerful when combined with State machines. The available links depend on the current State.

// Order State machine
const orderStates = {
  pending: {
    allowedTransitions: ["confirm", "cancel"],
    links: {
      confirm: { href: "/API/orders/{id}/confirm", method: "POST", title: "Confirm order" },
      cancel: { href: "/API/orders/{id}/cancel", method: "POST", title: "Cancel order" }
    }
  },
  confirmed: {
    allowedTransitions: ["ship", "cancel"],
    links: {
      ship: { href: "/API/orders/{id}/ship", method: "POST", title: "Mark as shipped" },
      cancel: { href: "/API/orders/{id}/cancel", method: "POST", title: "Cancel order" }
    }
  },
  shipped: {
    allowedTransitions: ["deliver"],
    links: {
      deliver: { href: "/API/orders/{id}/deliver", method: "POST", title: "Mark as delivered" }
    }
  },
  delivered: {
    allowedTransitions: ["return"],
    links: {
      return: { href: "/API/orders/{id}/return", method: "POST", title: "Return order" },
      review: { href: "/API/orders/{id}/review", method: "POST", title: "Write a review" }
    }
  },
  cancelled: {
    allowedTransitions: [],
    links: {
      reorder: { href: "/API/orders/{id}/reorder", method: "POST", title: "Reorder items" }
    }
  }
};

exports.getOrder = async (req, res) => {
  const order = await orderModel.findById(req.params.id);
  const stateConfig = orderStates[order.status];

  const response = {
    data: order,
    State: order.status,
    links: {
      self: { href: `/API/orders/${order.id}`, method: "GET" },
      ...stateConfig.links
    }
  };

  res.JSON(response);
};

Benefits and Trade-offs

Benefits

  • Evolvable APIs — Change URLs without breaking clients. Clients follow links instead of hardcoding URLs.
  • Self-documentingAPI responses describe available actions. No separate documentation needed.
  • Discoverable — Clients can explore the API by following links from the root entry point.
  • Reduced coupling — Server controls the navigation, not the client.

Trade-offs

  • Larger response payloads — Links add overhead to every response. For high-throughput APIs, the extra bytes matter.
  • Client complexity — Clients must parse links and navigate dynamically instead of using hardcoded URLs.
  • Fewer client libraries — Most API client libraries assume fixed URL structures and do not support dynamic link following.
  • Caching challenges — Links may change based on State, reducing cache effectiveness.

When to Use HATEOAS

Use Case Recommendation
Public REST APIs Full hypermedia with HAL or Siren
Internal Microservices Selective hypermedia for workflow APIs
Mobile app backends Minimal links (pagination, State transitions)
High-throughput APIs Skip hypermedia, use URL conventions instead

Common Errors

  1. Adding links to every response without context — Sending the same links regardless of resource State. Links should reflect available actions based on current State and user permissions.

  2. Hardcoding URLs in responses — Generating link URLs by string concatenation instead of using a link generator or router. Centralize URL generation to make API evolution easier.

  3. Not including documention for link relations — Using cryptic rel values like "rel": "next" without documentation. Use link relation types that point to documentation (CURIEs).

  4. Forgetting the entry point — Providing a HATEOAS API but no root endpoint that shows available starting actions. Every hypermedia API needs a root endpoint with initial navigation options.

  5. Overcomplicating simple CRUD APIs — Adding hypermedia to a basic CRUD API with five endpoints. Hypermedia adds value for complex workflows with State transitions, not simple data access.

  6. Not respecting content negotiation — Always returning the same format regardless of Accept headers. Support multiple media types (application/hal+JSON, application/vnd.dodatech+JSON) via content negotiation.

  7. Links that change unexpectedly — Generating different links for the same resource State between requests. Link generation must be deterministic based on resource State, not random or time-based.

Practice Questions

  1. What is the purpose of HATEOAS in REST architecture?
  2. How does HAL represent hypermedia controls?
  3. What is a CURIE and why is it useful?
  4. How does HATEOAS enable API evolution?
  5. When should you NOT use HATEOAS?

Challenge

Build a hypermedia-driven API for a task management system with State-based workflows. Tasks have states: todo, in-progress, review, done, archived. Each State allows specific transitions. Implement: HAL-formatted responses with _links and _embedded, a root entry point that discovers all available actions, CURIE-based link relation documentation, State-dependent action links (e.g., "start" only when in "todo"), and embedded resources for related entities. Include a simple client that navigates the API by following links.

FAQ

Is HATEOAS required for a RESTful API? According to Roy Fielding's dissertation, yes. An API that does not satisfy the HATEOAS constraint is not truly RESTful. In practice, most APIs claiming to be RESTful do not implement HATEOAS. These are better described as HTTP APIs or REST-like APIs.

Why do most APIs not implement HATEOAS? HATEOAS adds complexity to both server and client implementations. Most developers find the benefits (evolvable URLs, discoverability) do not justify the overhead for simple CRUD APIs. The practical use cases for HATEOAS are complex workflow APIs.

What media type should I use for HATEOAS? HAL (application/hal+JSON) is the most widely supported. JSON:API has built-in hypermedia support. Siren provides rich action descriptions. For simple APIs, a custom _links property in regular JSON is common.

How do clients handle hypermedia APIs? Clients start at the root URL and follow links from response to response. Instead of hardcoding /users/42, the client finds the "self" link in the user response. Generic hypermedia clients like Postman can parse HAL responses.

Does HATEOAS improve API performance? HATEOAS typically increases response sizes due to link payloads. However, it reduces the number of requests because responses can embed related resources via _embedded in HAL. The net performance impact depends on the API usage pattern.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro