HATEOAS (Hypermedia as the Engine of Application State)

Fundamentals Security Notes Jan 9, 2026 JSON
architecture rest hypermedia api-design discoverability state-management

Definition

HATEOAS (Hypermedia as the Engine of Application State) is the most advanced and often-ignored constraint of REST architecture. It means that clients interact with a REST API entirely through hypermedia links provided dynamically by the server. Instead of hardcoding URLs or following a fixed sequence of API calls from documentation, clients discover available actions by examining links in responses.

Think of HATEOAS like browsing the web. You don’t start with a list of every URL on a website - you land on the homepage and click links to navigate. If a page has a “Buy Now” button, you can click it. If not, that action isn’t available. HATEOAS brings this same discoverability to APIs: the server tells you what you can do next based on the current state.

This is the “glory of REST” (Level 3 in the Richardson Maturity Model) and the least-implemented constraint. Most APIs require clients to read documentation and hardcode URL patterns, violating HATEOAS. A truly RESTful API lets clients navigate purely by following links, making the API self-documenting and enabling servers to change URL structures without breaking clients.

Example

GitHub Pull Request Workflow: When you fetch a pull request via GET /repos/:owner/:repo/pulls/:number, GitHub returns the PR data plus hypermedia links based on state. If the PR is open and mergeable, the response includes "mergeable": true and a "merge_url" link. If it’s already merged, that link disappears, and a "commits_url" appears instead. The client doesn’t need to know the PR workflow - it follows the links provided.

PayPal Payment Approval Flow: When you create a payment via PayPal, the response includes "approval_url": "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-XXXX". You redirect the user there (following the link), PayPal handles approval, then returns the user with a new link to execute the payment: "execute": "/v1/payments/payment/PAY-123/execute". Each state transition is guided by hypermedia - no hardcoded URLs needed.

Stripe Checkout Session: Stripe’s Checkout API creates a session with POST /v1/checkout/sessions. The response includes "url": "https://checkout.stripe.com/pay/cs_test_XXXX" - you redirect the user to this URL (hypermedia link). After payment, Stripe redirects back with success or cancel URLs you provided. The client follows links, the server controls the flow.

Order Lifecycle: Imagine an e-commerce API where orders transition through states: pending β†’ paid β†’ shipped β†’ delivered. A pending order includes links: "pay": "/orders/123/payment", "cancel": "/orders/123". Once paid, the pay link disappears, cancel may remain, and a new "track_shipment": "/orders/123/tracking" link appears. The available actions change with state, all communicated via hypermedia.

Amazon Product Recommendation: When viewing a product, Amazon’s internal APIs likely return related product links: "frequently_bought_together": ["/products/456", "/products/789"], "customers_also_viewed": ["/products/111", "/products/222"]. The client doesn’t calculate these - it follows the server-provided links to navigate the product catalog.

Analogy

The Guided Museum Tour: You enter a museum and get an audio guide. At each exhibit, the guide tells you what you can do next: “Turn left for Renaissance Art, turn right for Modern Sculpture, or go upstairs for the Rooftop Cafe.” You don’t need a map - the guide (hypermedia) directs you based on your current location (state). If the Rooftop Cafe is closed, that option isn’t mentioned. HATEOAS works the same way - the server guides you through available actions.

The ATM Interface: When you insert your card, the ATM shows available options: Withdraw, Deposit, Check Balance. If your account is overdrawn, “Withdraw” might not appear. After selecting “Withdraw,” the ATM guides you to the next screen (enter amount), then the next (take cash), then offers new options (print receipt?). You navigate by following prompts (links), not by memorizing button sequences.

GPS Turn-by-Turn Navigation: GPS doesn’t give you the entire route upfront. It tells you the next turn, then the next, adapting if you take a wrong turn. Each instruction is based on your current state (location). HATEOAS provides the same dynamic, state-based guidance - each response includes the next valid steps.

The Adventure Video Game: In games like The Legend of Zelda, you can’t access areas until you’ve completed prerequisites. The game shows you available paths (hypermedia links) based on your progress (state). If you don’t have the hookshot, cliff edges aren’t climbable. As you gain items, new paths open. HATEOAS makes APIs behave like this - available actions emerge as state changes.

Code Example

// Example: Order Workflow with HATEOAS
// Each state provides different hypermedia links

// ===== STATE 1: Order Created (Pending Payment) =====
// GET /api/orders/789

{
  "id": 789,
  "user_id": 123,
  "status": "pending",
  "total": 59.98,
  "created_at": "2026-01-09T10:30:00Z",

  // HATEOAS: Available actions based on current state
  "_links": {
    "self": {
      "href": "/api/orders/789",
      "method": "GET",
      "rel": "self"
    },
    "user": {
      "href": "/api/users/123",
      "method": "GET",
      "rel": "related"
    },
    "items": {
      "href": "/api/orders/789/items",
      "method": "GET",
      "rel": "collection"
    },
    // Action: Pay for the order
    "pay": {
      "href": "/api/orders/789/payment",
      "method": "POST",
      "rel": "action"
    },
    // Action: Update order details
    "update": {
      "href": "/api/orders/789",
      "method": "PATCH",
      "rel": "edit"
    },
    // Action: Cancel the order
    "cancel": {
      "href": "/api/orders/789",
      "method": "DELETE",
      "rel": "delete"
    }
  }
}

// ===== STATE TRANSITION: Client follows "pay" link =====
// POST /api/orders/789/payment
// { "payment_method": "card", "token": "tok_visa" }

// Server processes payment and transitions order to "paid" state

// ===== STATE 2: Order Paid =====
// GET /api/orders/789

{
  "id": 789,
  "user_id": 123,
  "status": "paid",
  "total": 59.98,
  "created_at": "2026-01-09T10:30:00Z",
  "paid_at": "2026-01-09T10:35:00Z",

  // HATEOAS: Different links - state changed!
  "_links": {
    "self": {
      "href": "/api/orders/789",
      "method": "GET",
      "rel": "self"
    },
    "user": {
      "href": "/api/users/123",
      "method": "GET",
      "rel": "related"
    },
    "items": {
      "href": "/api/orders/789/items",
      "method": "GET",
      "rel": "collection"
    },
    // "pay" link removed (already paid)
    // "update" link removed (can't modify paid order)
    // "cancel" link removed (can't cancel paid order)

    // New links based on new state:
    "invoice": {
      "href": "/api/orders/789/invoice.pdf",
      "method": "GET",
      "rel": "related"
    },
    "track_shipment": {
      "href": "/api/orders/789/tracking",
      "method": "GET",
      "rel": "related"
    },
    "request_refund": {
      "href": "/api/orders/789/refund",
      "method": "POST",
      "rel": "action"
    }
  }
}

// ===== STATE 3: Order Shipped =====
// After warehouse ships the order
// GET /api/orders/789

{
  "id": 789,
  "user_id": 123,
  "status": "shipped",
  "total": 59.98,
  "created_at": "2026-01-09T10:30:00Z",
  "paid_at": "2026-01-09T10:35:00Z",
  "shipped_at": "2026-01-09T14:20:00Z",
  "tracking_number": "1Z999AA10123456784",

  "_links": {
    "self": {
      "href": "/api/orders/789",
      "method": "GET",
      "rel": "self"
    },
    "user": {
      "href": "/api/users/123",
      "method": "GET",
      "rel": "related"
    },
    "items": {
      "href": "/api/orders/789/items",
      "method": "GET",
      "rel": "collection"
    },
    "invoice": {
      "href": "/api/orders/789/invoice.pdf",
      "method": "GET",
      "rel": "related"
    },
    // Enhanced tracking now available
    "tracking": {
      "href": "/api/orders/789/tracking",
      "method": "GET",
      "rel": "related"
    },
    "tracking_external": {
      "href": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
      "method": "GET",
      "rel": "external"
    },
    // Refund still possible but more restricted
    "request_refund": {
      "href": "/api/orders/789/refund",
      "method": "POST",
      "rel": "action",
      "note": "Refund available within 30 days of delivery"
    }
  }
}

// ===== CLIENT CODE: Navigating via HATEOAS =====
// Client doesn't hardcode URLs - it follows links

async function navigateOrder(orderId) {
  // Start by fetching the order
  let response = await fetch(`/api/orders/${orderId}`);
  let order = await response.json();

  // Client examines available links
  if (order._links.pay) {
    // "pay" link exists, so payment is possible
    console.log("Order is pending payment");
    console.log("Pay at:", order._links.pay.href);
  }

  if (order._links.track_shipment) {
    // "track_shipment" link exists, so order is shipped or in transit
    console.log("Order is shipped");
    console.log("Track at:", order._links.track_shipment.href);
  }

  // Client follows links dynamically
  if (order._links.invoice) {
    // Download invoice by following link
    const invoiceUrl = order._links.invoice.href;
    window.open(invoiceUrl, '_blank');
  }

  // No hardcoded URLs - all navigation via hypermedia
}

Diagram

stateDiagram-v2
    [*] --> Created: POST /api/orders

    state Created {
        [*] --> Links1
        Links1: _links.pay
        Links1: _links.update
        Links1: _links.cancel
    }

    Created --> Paid: Follow pay link
POST /api/orders/789/payment state Paid { [*] --> Links2 Links2: _links.invoice Links2: _links.track_shipment Links2: _links.request_refund Links2: ❌ pay (removed) Links2: ❌ cancel (removed) } Paid --> Shipped: Warehouse ships
Server updates state state Shipped { [*] --> Links3 Links3: _links.tracking Links3: _links.tracking_external Links3: _links.invoice Links3: _links.request_refund* } Shipped --> Delivered: Delivery confirmed state Delivered { [*] --> Links4 Links4: _links.invoice Links4: _links.leave_review Links4: _links.reorder Links4: ❌ request_refund (30 days passed) } Delivered --> [*] Created --> Cancelled: Follow cancel link
DELETE /api/orders/789 Cancelled --> [*] note right of Created Client discovers actions by examining _links end note note right of Paid Available actions change based on state end note note right of Shipped Hypermedia guides client through workflow end note

Security Notes

SECURITY NOTES

CRITICAL: HATEOAS inadvertently exposes sensitive information through available action links.

Link Authorization:

  • Filter links by permission: Only include action links the user can perform
  • Don’t expose link existence: Don’t reveal links for unauthorized actions
  • Server-side validation required: Validate permissions even if client discovered a link manually
  • Prevent URL construction: Clients can manually construct URLs; validate all requests

Link Security:

  • Signed/tokenized links: Use signed or time-limited tokens for sensitive operations
  • Link expiration: Time-limit action links (e.g., payment approval links)
  • CSRF protection: Include CSRF tokens in state-changing links when using cookies
  • Validate link source: Ensure links haven’t been tampered with

Information Disclosure Prevention:

  • Hide internal IDs: Don’t expose database IDs or internal resource identifiers
  • Hide business logic: Don’t reveal implementation details through link structures
  • Minimize metadata: Avoid exposing relationships and internal structure

Enumeration & Reconnaissance:

  • Rate limit link discovery: Prevent attackers from enumerating available operations
  • Log traversal patterns: Monitor suspicious patterns of link traversal and exploration
  • Validate ownership: Ensure users can only access links for resources they own

Transport Security:

  • HTTPS exclusively: Protect links in transit (links reveal structure and resource IDs)
  • No unencrypted links: Never transmit sensitive links over HTTP

Best Practices

  1. Include links for all valid state transitions: Only show actions the resource can currently perform
  2. Use standard link relation types: self, related, next, prev, edit, delete from RFC 5988
  3. Remove unavailable actions: Don’t show “cancel” link if order is already shipped
  4. Include method information: Specify GET, POST, PUT, DELETE for each link
  5. Use absolute URLs: Avoid relative paths requiring client-side construction
  6. Follow hypermedia standards: Use HAL, JSON:API, Siren, or Collection+JSON
  7. Make links self-documenting: Use descriptive rel attributes and optional title fields
  8. Implement link templates: Use RFC 6570 URI templates for parameterized links
  9. Provide forms for complex actions: Include schema information for POST/PUT payloads
  10. Start simple, evolve: Begin with basic HATEOAS and add complexity as needed

Common Mistakes

Including all possible links regardless of state: Showing “pay” link even when order is already paid violates HATEOAS principles.

Hardcoding URLs in client code: Defeats the purpose of HATEOAS and creates tight coupling.

Missing rel attributes: Clients can’t understand link purpose without relation types.

Inconsistent link formats: Mixing different hypermedia standards confuses clients.

No documentation of link relations: Custom rel values without documentation are useless.

Ignoring authorization: Showing links for actions the user isn’t allowed to perform is a security risk.

Relative URLs without base: Providing /orders/123 instead of https://api.example.com/orders/123 requires clients to know the base URL.

Static documentation instead of dynamic discovery: Writing “to pay an order, POST to /orders/:id/payment” in docs instead of providing the link dynamically.

Breaking link contracts: Changing link structures between API versions breaks clients that follow links.

Over-engineering: Implementing complex HATEOAS for simple, stable APIs where it adds little value.

Standards & RFCs

Standards & RFCs
4)- HAL - Hypertext Application Language
5)- JSON:API - JSON API Specification
6)- Siren - Hypermedia Specification
7)- Collection+JSON