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_linksfor links and_embeddedfor nested resources. - Pagination with HATEOAS is clean: include
next,prev,first,lastlinks 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.
