Definition
- HATEOAS – Hypermedia As The Engine Of Application State, the REST constraint where responses include links describing the actions available next.
- Links – each link has a relation name (
rel) and a URL (href); the client follows links by rel rather than hardcoding URLs. - State-driven – links are dynamic: an active account includes a transfer link; a frozen account omits it.
- HAL – Hypertext Application Language, a common media type for hypermedia responses with an
_linksobject. - Why skip – writing a client that discovers behaviour from links is harder than calling documented URLs; most clients hardcode URLs anyway.
- Practical middle – include useful links like pagination (next, prev) without committing to full hypermedia.
Imagine Rajesh checks his account at National Bank of India and the statement comes with a menu at the bottom: "to transfer, use this counter," "to close this account, use that counter." He never memorises counter numbers; the statement tells him what he can do next. That is HATEOAS. In reality, most banks just give a guidebook once (the OpenAPI documentation), and Rajesh learns the counters from it, because reprinting the menu on every statement is more trouble than it is worth.
Understanding the Concepts
What is HATEOAS?
HATEOAS stands for Hypermedia As The Engine Of Application State. It is the REST constraint that says a response should include links describing the actions a client can take next. The client discovers operations from the response rather than hardcoding URLs. This is the highest level of the Richardson Maturity Model.
// HATEOAS response: links tell the client what it can do next.
// GET /api/v1/accounts/SAV2001
// {
// "accountId": "SAV2001",
// "balance": 1000000.00,
// "status": "ACTIVE",
// "_links": {
// "self": {"href": "/api/v1/accounts/SAV2001"},
// "transactions": {"href": "/api/v1/accounts/SAV2001/transactions"},
// "transfer": {"href": "/api/v1/accounts/SAV2001/transfers"},
// "freeze": {"href": "/api/v1/accounts/SAV2001/freeze"}
// }
// }
How does HATEOAS use state-driven links?
The crucial idea is that links are state-driven. The server includes only the links valid for the resource's current state. An active account includes transfer and freeze links. A frozen account includes an unfreeze link and omits transfer. The client renders buttons for the links present and never replicates the server's business rules.
package com.bank.controller;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountHateoasController {
@GetMapping("/{id}")
public EntityModel<AccountResponse> get(@PathVariable String id) {
AccountResponse dto = accountService.findById(id);
EntityModel<AccountResponse> model = EntityModel.of(dto);
model.add(linkTo(methodOn(AccountHateoasController.class).get(id)).withSelfRel());
model.add(linkTo(methodOn(TransactionController.class).list(id, null, 20))
.withRel("transactions"));
if ("ACTIVE".equals(dto.status())) {
model.add(linkTo(methodOn(AccountHateoasController.class).freeze(id))
.withRel("freeze"));
model.add(linkTo(methodOn(TransferController.class).create(null, null))
.withRel("transfer"));
} else if ("FROZEN".equals(dto.status())) {
model.add(linkTo(methodOn(AccountHateoasController.class).unfreeze(id))
.withRel("unfreeze"));
}
return model;
}
}
What is HAL format?
HAL (Hypertext Application Language) is a common media type for hypermedia responses. It represents links under an _links object, mapping relation names to objects with an href. Spring HATEOAS uses HAL as its default format.
// HAL response structure:
// {
// "accountId": "SAV2001",
// "balance": 1000000.00,
// "_links": {
// "self": {"href": "/api/v1/accounts/SAV2001"},
// "transactions": {"href": "/api/v1/accounts/SAV2001/transactions"}
// }
// }
Why do most real-world APIs skip full HATEOAS?
Most production APIs skip full HATEOAS for several reasons.
First, client complexity: writing a client that genuinely discovers behaviour by parsing links at runtime is much harder than calling known URLs. In practice, almost all clients hardcode the documented URLs and ignore the links.
Second, the benefit is delivered more cheaply by OpenAPI: it describes every endpoint, parameter, and response in a machine-readable spec that generates SDKs and interactive docs.
Third, payload bloat: every response carries link metadata, and the server must compute which transitions are valid.
| Aspect | Full HATEOAS | OpenAPI Documentation |
|---|---|---|
| Client coupling | Loose: follows links | Moderate: uses documented URLs |
| Client complexity | High: must parse links | Low: calls known URLs |
| SDK generation | No | Yes |
| Server overhead | Must compute valid links | None |
| Adoption | Rare | Ubiquitous |
// Full HATEOAS: server computes links, client follows them.
// In practice, the mobile team hardcodes the URLs and ignores the links.
// OpenAPI: one spec file generates SDKs for Java, TypeScript, Python.
// This is what teams actually use.
Where does HATEOAS add genuine value?
Pagination is the one place HATEOAS ideas are widely adopted. Next and previous links make paging trivial for the client. State-dependent action links also add value when the server's state machine is complex and the client should not replicate it.
// Pagination links: widely adopted and useful.
// {
// "content": [...],
// "_links": {
// "self": {"href": "/transactions?page=2&size=20"},
// "next": {"href": "/transactions?page=3&size=20"},
// "prev": {"href": "/transactions?page=1&size=20"}
// }
// }
How do you build HATEOAS with Spring HATEOAS?
Spring HATEOAS provides EntityModel (wraps a DTO with links), CollectionModel (wraps a collection), and the linkTo/methodOn helpers that build links from controller methods in a type-safe way.
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.CollectionModel;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
// Single resource with links:
EntityModel<AccountResponse> model = EntityModel.of(dto);
model.add(linkTo(methodOn(AccountController.class).get(id)).withSelfRel());
// Collection with pagination links:
CollectionModel<EntityModel<TransactionResponse>> collection =
CollectionModel.of(items);
collection.add(linkTo(methodOn(TransactionController.class)
.list(accountId, nextPage, size)).withRel("next"));
What is the practical recommendation?
The practical default is a well-versioned, well-documented REST API described by OpenAPI, sprinkled with the few links that add genuine value: pagination links for next/previous, and state-dependent action links where the server's state machine is complex.
// Recommended approach: OpenAPI + selective links.
// 1. Document the full API with OpenAPI/Swagger.
// 2. Add pagination links (next, prev, self).
// 3. Add state-dependent links where they prevent client-side rule duplication.
// 4. Do NOT commit to full hypermedia-driven navigation.
What should a senior engineer know about HATEOAS?
A senior engineer knows that HATEOAS optimises for long-lived, generic clients that evolve independently of the server, like a web browser. Most business APIs have a known, co-evolving set of clients where versioning and documentation solve coupling more directly. They apply HATEOAS where state-dependent discoverability genuinely helps rather than as dogma everywhere.
Visual Overview
┌──────────────────────────────────────────────────────────┐ │ HATEOAS State-Driven Links │ ├──────────────────────────────────────────────────────────┤ │ │ │ Account status: ACTIVE │ │ ┌──────────────────────────────────────────────┐ │ │ │ Response links: │ │ │ │ self → /accounts/SAV2001 │ │ │ │ transactions → /accounts/SAV2001/txns │ │ │ │ transfer → /accounts/SAV2001/transfers │ ◀ present│ │ │ freeze → /accounts/SAV2001/freeze │ ◀ present│ │ └──────────────────────────────────────────────┘ │ │ │ │ Account status: FROZEN │ │ ┌──────────────────────────────────────────────┐ │ │ │ Response links: │ │ │ │ self → /accounts/SAV2001 │ │ │ │ transactions → /accounts/SAV2001/txns │ │ │ │ unfreeze → /accounts/SAV2001/unfreeze │ ◀ present│ │ │ (transfer and freeze ABSENT) │ ◀ removed│ │ └──────────────────────────────────────────────┘ │ │ │ │ Client logic: render buttons for links present. │ │ No need to know business rules about state transitions. │ └──────────────────────────────────────────────────────────┘
Key Takeaway
HATEOAS means responses carry links to the next possible actions so the client discovers operations from the response rather than hardcoding URLs. In practice, links are state-driven: an active account carries a transfer link, a frozen account omits it and shows unfreeze instead. In Spring, you build this with EntityModel and the linkTo/methodOn helpers that generate links from controller methods in a type-safe way.
Most real-world REST APIs skip full HATEOAS because the complexity outweighs the benefit. Writing a client that discovers behaviour by parsing links is much harder than calling documented URLs, and in practice almost every client hardcodes the URLs and ignores the links. The coupling benefit HATEOAS promises is delivered more cheaply by OpenAPI, which describes every endpoint in a machine-readable spec that generates SDKs, interactive docs, and server stubs.
At National Bank of India, the team evaluated full HATEOAS on the accounts API. They built a HAL version where the account response carried conditional links. But when the mobile and partner teams integrated, they ignored the links and hardcoded documented URLs, because their clients were purpose-built, not generic hypermedia agents. The one place links clearly helped was pagination: next and previous links made paging trivial. So the team kept pagination links, dropped the rest of the hypermedia machinery, and documented the full contract with OpenAPI.
The judgement is that HATEOAS optimises for long-lived generic clients that evolve independently, like a web browser. Most business APIs have a known, co-evolving set of clients where versioning and documentation solve coupling more directly. The practical default is a well-versioned, well-documented REST API sprinkled with the few links that add genuine value.
Remember one sentence: HATEOAS embeds state-driven links in responses so clients discover actions instead of hardcoding them, but most APIs adopt it selectively for pagination and state-dependent actions rather than committing to full hypermedia.
---