HATEOAS in JSON REST APIs: HAL, Siren, JSON:API, and the Hypermedia Constraint
Last updated:
HATEOAS — Hypermedia As The Engine Of Application State — is the REST constraint that says servers should hand clients the next legal URLs to follow, instead of clients constructing those URLs from documentation. It is the one constraint of Roy Fielding's 2000 dissertation that most APIs labeled REST quietly ignore. When HATEOAS is present, a response carries a _links block (in HAL), an actions block (in Siren), or a links/relationships structure (in JSON:API), and the client navigates by following those links by relation name rather than by hardcoding paths. The payoff is server-controlled URL evolution and runtime advertisement of legal actions; the cost is more complex clients and one extra round-trip on cold start. This guide covers what HATEOAS actually is, the three main JSON hypermedia media types, server library options, client traversal patterns, and the cases where HATEOAS earns its keep versus where simpler URL-template APIs win.
Designing a HAL or JSON:API response and the JSON keeps breaking? Drop it into Jsonic's JSON Validator — it catches the trailing commas inside _links objects and bracket mismatches in_embedded arrays that bite every hypermedia rollout.
What HATEOAS actually means (Roy Fielding's constraint)
HATEOAS is one of six architectural constraints Roy Fielding defined for REST in his 2000 University of California, Irvine PhD dissertation, Architectural Styles and the Design of Network-based Software Architectures. The full name — Hypermedia As The Engine Of Application State — packs the idea into one phrase: the client's understanding of what it can do next comes from hypermedia controls in the current response, not from out-of-band documentation.
Concretely, every response includes links (and sometimes forms or action descriptors) for the legal transitions from the current resource state. A pending order resource might include links for cancel and edit-shipping; once it ships, the same resource includes track but no longer cancel. The client never asks "can I cancel this?" — it checks whether thecancel link is present. State machine rules live on the server.
Fielding has been emphatic about this constraint. In a widely cited 2008 blog post he wrote that any API claiming to be REST without hypermedia controls in its responses is misusing the term. The community largely ignored him; most APIs labeled REST in 2026 satisfy the other five constraints (client-server separation, statelessness, cacheability, uniform interface, layered system) but use URL templates documented in OpenAPI rather than runtime hypermedia. That is fine engineering — it is just not REST in the original sense. Read about practical REST API design for the broader context of how HATEOAS sits alongside the other design decisions.
Why most "REST" APIs aren't actually RESTful (the Richardson Maturity Model)
Leonard Richardson's Maturity Model, popularized by Martin Fowler in 2010, gives the industry a vocabulary for how much of REST any given API actually uses. The model has four levels.
| Level | What it adds | Typical example |
|---|---|---|
| Level 0 | HTTP as a tunnel for RPC — one URL, one method (usually POST), method name in the body | SOAP, XML-RPC, JSON-RPC |
| Level 1 | Resources — separate URLs per entity (/users/123, /orders/456) | Most internal APIs c. 2005 |
| Level 2 | HTTP verbs — GET reads, POST creates, PUT/PATCH updates, DELETE removes; status codes carry meaning | Stripe, Twilio, most modern APIs labeled REST |
| Level 3 | HATEOAS — responses include links advertising legal next actions | PayPal v2, Spring HATEOAS demos, AtomPub |
Most APIs that ship with documentation describing endpoint paths and methods are at Level 2. They are useful and well-designed and they handle billions of requests per day. They are not REST by Fielding's original definition. The Maturity Model is the diplomatic way to point this out without arguing about terminology: an API can be Level 2 and be excellent.
Level 3 is rare in practice for the reasons covered in the FAQ — single-producer teams gain little from decoupling URLs from clients they also write, and OpenAPI made static URL documentation good enough for runtime discovery. Where Level 3 wins is public APIs with many independent clients, federated systems, and resources with complex state machines.
HAL (Hypertext Application Language): _links and _embedded
HAL is the minimal JSON hypermedia format. The entire specification adds two reserved keys to your existing JSON: _links for hyperlinks to related resources and _embedded for related resources inlined directly into the response. Everything else in the JSON document is your normal application data. HAL ships under the media types application/hal+json and application/hal+xml (the JSON variant is overwhelmingly more common). The spec is a single Internet-Draft (Kelly, 2012) that has remained stable for over a decade.
HTTP/1.1 200 OK
Content-Type: application/hal+json
{
"_links": {
"self": { "href": "/orders/523" },
"customer": { "href": "/customers/2" },
"cancel": { "href": "/orders/523/cancel" },
"ex:items": { "href": "/orders/523/items" }
},
"_embedded": {
"customer": {
"_links": { "self": { "href": "/customers/2" } },
"name": "Alice",
"email": "alice@example.com"
}
},
"id": 523,
"status": "pending",
"total": 4250
}Every _links entry is keyed by a link relation (rel). Use IANA-registered rels (self, next, prev, first, last, up, related) where they exist and namespaced custom rels (ex:items, ex:approve) with a CURIE declaration for everything else. The CURIE block lives under _links.curies and maps the prefix to a documentation URL the client can dereference.
_embedded exists for performance: inline a related resource the client will almost certainly request next, and you save the round-trip. The embedded resource is itself a full HAL document with its own _links, so the client can keep navigating without going back to the network. See our JSON hypermedia patterns guide for deeper treatment of _embedded trade-offs.
Siren: classes, properties, entities, actions
Siren is a richer hypermedia format from Kevin Swiber that ships under application/vnd.siren+json. Where HAL describes only what you can navigate to, Siren describes both navigation and state-changing operations, including the field shape each operation expects — closer to HTML forms than HAL's link-only model. A Siren entity has five top-level fields: class (semantic type tags), properties (the resource data), entities (related sub-resources, equivalent to HAL _embedded), links (hyperlinks like HAL _links), and actions (state-changing operations with method, href, and field descriptors).
HTTP/1.1 200 OK
Content-Type: application/vnd.siren+json
{
"class": ["order"],
"properties": {
"orderNumber": 523,
"status": "pending",
"total": 4250
},
"entities": [
{
"class": ["customer"],
"rel": ["http://x.io/rels/customer"],
"href": "/customers/2",
"properties": { "name": "Alice" }
}
],
"links": [
{ "rel": ["self"], "href": "/orders/523" }
],
"actions": [
{
"name": "cancel-order",
"title": "Cancel Order",
"method": "POST",
"href": "/orders/523/cancel",
"type": "application/json",
"fields": [
{ "name": "reason", "type": "text", "title": "Reason for cancellation" }
]
},
{
"name": "add-item",
"method": "POST",
"href": "/orders/523/items",
"fields": [
{ "name": "productId", "type": "text" },
{ "name": "quantity", "type": "number", "value": "1" }
]
}
]
}The actions array is the distinguishing feature. A generic client can read it and build a UI on the fly — render an HTML form, an admin tool checkbox, or a Postman-like request builder, all from server-provided metadata. HAL cannot do this without out-of-band documentation per relation. The cost is verbosity: Siren responses are roughly twice the size of equivalent HAL responses, and the spec has a steeper learning curve. Use Siren when you genuinely need machine-readable form descriptors; otherwise HAL is the better default.
JSON:API links and relationships
JSON:API (application/vnd.api+json) is a stricter specification that standardizes far more than HAL or Siren: pagination, sparse fieldsets, sorting, filtering, errors, and — relevant here — links and relationships. JSON:API includes HATEOAS by default; every primary resource and every relationship can carry a links object, and the spec defines self and related link semantics explicitly. See our JSON:API spec guide for the full surface, and the JSON:API implementation walkthrough for a concrete server build.
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"links": {
"self": "/orders/523"
},
"data": {
"type": "orders",
"id": "523",
"attributes": {
"status": "pending",
"total": 4250
},
"relationships": {
"customer": {
"links": {
"self": "/orders/523/relationships/customer",
"related": "/orders/523/customer"
},
"data": { "type": "customers", "id": "2" }
},
"items": {
"links": {
"self": "/orders/523/relationships/items",
"related": "/orders/523/items"
}
}
}
},
"included": [
{
"type": "customers",
"id": "2",
"attributes": { "name": "Alice" },
"links": { "self": "/customers/2" }
}
]
}The relationships object is JSON:API's answer to HAL's _links: every relationship gets a self link (for modifying the linkage itself: PATCH the relationship to change which customer owns this order) and a related link (for fetching the related resource).included plays the role of HAL's _embedded — inline the related resource to save a round-trip. Unlike HAL, JSON:API does not have a dedicated actions field; state transitions are expressed as PATCH operations against the resource itself or its relationships, and the available transitions are documented out-of-band (typically in OpenAPI). This is a deliberate choice — JSON:API targets Level 2-plus, not full Siren-style Level 3.
Spring HATEOAS, hal_json (Python), Express middleware
Server-side HATEOAS support varies sharply by ecosystem. Java/Spring has the most mature toolchain; Python and Node have good libraries but less framework integration; Go and Rust mostly hand-roll.
Spring HATEOAS is the reference implementation in the Java world. It provides RepresentationModel, EntityModel, and CollectionModel wrappers that ship under HAL by default, plus a WebMvcLinkBuilder that builds links from controller method references, so refactoring a controller method updates every link that points to it.
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{id}")
public EntityModel<Order> one(@PathVariable Long id) {
Order order = orderRepo.findById(id).orElseThrow();
EntityModel<Order> model = EntityModel.of(order);
model.add(linkTo(methodOn(OrderController.class).one(id)).withSelfRel());
model.add(linkTo(methodOn(CustomerController.class).one(order.getCustomerId()))
.withRel("customer"));
if (order.getStatus() == Status.PENDING) {
model.add(linkTo(methodOn(OrderController.class).cancel(id)).withRel("cancel"));
}
return model;
}
}Python options include flask-hal, hal_json, and pyrsistent-hal for HAL, plus flask-rest-jsonapi and jsonapi-utils for JSON:API. Django REST Framework does not ship HATEOAS-aware serializers by default, but the HyperlinkedModelSerializer produces resource URLs (a partial Level 3 affordance). Node.js options include halson and express-hal-mw for HAL, siren-writer for Siren, and jsonapi-server for JSON:API. Building hypermedia responses by hand in any language is not difficult — the_links shape is small — and many teams skip the library and emit the JSON directly.
Client patterns: traversing links, following relations
A HATEOAS client starts at a single entry point — the API root — and follows links by relation name to reach every other resource. The client's only hardcoded URL is the root. Library support for this pattern is uneven; the most common ones are Traverson (Java, Spring HATEOAS), ketting (JavaScript), and hal-client (Python).
// JavaScript with raw fetch — recursive HAL traversal
async function followRel(currentUrl, ...rels) {
let res = await fetch(currentUrl, {
headers: { Accept: 'application/hal+json' }
})
let body = await res.json()
for (const rel of rels) {
const next = body._links?.[rel]
if (!next) throw new Error(`no ${rel} link on ${currentUrl}`)
res = await fetch(next.href, {
headers: { Accept: 'application/hal+json' }
})
body = await res.json()
}
return body
}
// Usage: walk from API root to a customer's most recent order
const order = await followRel(
'https://api.example.com/',
'customers', // root -> /customers
'first', // /customers -> /customers/2 (or pagination)
'ex:latest-order' // customer -> their latest order
)Three rules keep client code from breaking on link-shape changes. First, never construct URLs in client code — only follow links the server returned. Second, check link presence before assuming an action is legal — the absence of acancel link means cancel is not currently legal, full stop. Third, cache the root response aggressively; without that the cold-start extra round-trip becomes noticeable, with it the overhead disappears in steady state.
For Content negotiation, always send Accept: application/hal+json (or the equivalent for Siren/JSON:API) so a server that supports multiple formats returns the hypermedia variant. Some servers default to plain JSON without the right Accept header.
When HATEOAS is overkill vs when it shines (public APIs, federated systems)
HATEOAS pays for itself in three situations and costs more than it earns in everything else. The honest answer is that most APIs are in the second bucket.
HATEOAS wins for:
- Long-lived public APIs with many independent clients — when you cannot coordinate URL changes with every consumer, hypermedia lets you rename and reorganize without breaking them. PayPal v2 and several IETF protocols use this model.
- Federated systems — when multiple producers expose interlinked resources (think a multi-org collaboration platform), every response is essentially a graph node, and explicit links between nodes are the only sensible way to navigate.
- Complex state machines — resources whose legal actions depend on state (orders, claims, workflows). Encoding the state machine in link presence is cleaner than a parallel
allowedActions: []array.
HATEOAS is overkill for:
- Single-producer CRUD APIs — when the same team owns both server and client, URL templates plus OpenAPI plus semantic versioning give you the same decoupling with less protocol surface.
- RPC-shaped workloads — payments, search, ML inference. These have no meaningful resource graph; the API is verbs, not nouns. HATEOAS adds ceremony without adding value.
- Internal microservices — service-to-service traffic where both sides ship together. Just use URL templates and version the contract.
For a typical REST API JSON response from an internal service, plain JSON with documented URLs is usually the right call. Reach for HAL or JSON:API when the situation matches one of the three winning scenarios above — not because someone on the team read Fielding's dissertation.
Key terms
- HATEOAS
- Hypermedia As The Engine Of Application State. The REST constraint that requires servers to advertise legal next actions via hypermedia controls (links, forms) inside each response, rather than clients hardcoding URLs from out-of-band documentation.
- link relation (rel)
- A short string naming the semantic relationship between two resources —
self,next,related, or a custom namespaced rel likeex:cancel. IANA maintains a registry of standard rel names. - HAL
- Hypertext Application Language. A minimal JSON hypermedia format from Mike Kelly (Internet-Draft, 2012) that adds two reserved keys to any JSON document:
_linksfor hyperlinks and_embeddedfor inlined related resources. Media typeapplication/hal+json. - Siren
- A richer hypermedia format from Kevin Swiber under
application/vnd.siren+jsonthat addsclass,properties,entities,links, andactions. Theactionsarray describes state-changing operations with HTTP method, URL, and field descriptors. - JSON:API
- A specification (
application/vnd.api+json) for shaping JSON resource responses with standardized links, relationships, included sub-resources, pagination, errors, and sparse fieldsets. Includes HATEOAS-style links by default. - Richardson Maturity Model
- A four-level model (0–3) describing how much of REST an API uses. Level 0 is HTTP-as-RPC, Level 1 adds resources, Level 2 adds verbs and status codes, Level 3 adds HATEOAS. Most modern APIs labeled REST are Level 2.
Frequently asked questions
What is HATEOAS in REST?
HATEOAS stands for Hypermedia As The Engine Of Application State. It is the constraint Roy Fielding added to REST in his 2000 PhD dissertation that says clients should not hard-code URL templates. Instead, each response from the server includes links to the next legal actions, and the client discovers what it can do by reading those links at runtime. A bank account resource might include a withdraw link only when the balance is positive and a close link only when the balance is zero. The client does not need to know the business rule — the server omits links for actions that are not currently legal. The practical payoff is that the server can rename URLs, add new states, or change preconditions without breaking clients that already follow links instead of constructing them. Most APIs labeled REST today skip this constraint entirely, which is why Fielding wrote in 2008 that they should not be called REST.
Is HATEOAS the same as REST?
No — HATEOAS is one of six constraints REST requires, but it is the one most APIs ignore. The other five (client-server, stateless, cacheable, uniform interface, layered system) are easy to satisfy by accident when you build any HTTP JSON API. HATEOAS is the hard one: it requires every response to carry the hypermedia controls (links, forms, actions) the client needs to drive the next request. Roy Fielding has been explicit since 2008 that an API without HATEOAS is not REST, even if it uses HTTP verbs and resource URLs. The industry settled on calling such APIs Level 2 REST (per the Richardson Maturity Model), which is more accurate than calling them RESTful. Whether you care about the strict definition is a separate question — many successful APIs are Level 2 and work fine — but the terminology distinction matters when evaluating designs against the original definition.
What's the difference between HAL and Siren?
HAL (Hypertext Application Language) is minimal: a resource has two reserved keys, _links for hyperlinks and _embedded for related resources inlined into the response. That is the entire spec — everything else is regular JSON. Siren is richer: a resource has class (semantic type tags), properties (the resource data), entities (related sub-resources, like _embedded), links (like _links), and actions (full HTTP method + URL + field descriptors for state transitions, similar to HTML forms). HAL describes what you can navigate to; Siren describes both what you can navigate to and what state-changing operations are legal right now, including the field shape each operation expects. HAL ships under application/hal+json and is the more widely deployed of the two. Siren ships under application/vnd.siren+json and is the better fit when clients need machine-readable form descriptors — for example, a generic admin UI built from API metadata.
Do public APIs (GitHub, Stripe) use HATEOAS?
GitHub uses a semi-HATEOAS pattern: many resources include a _links-style block (called url plus suffixed *_url fields like issues_url, blob_url, commits_url) that points to related resources, and pagination uses RFC 5988 Link headers. Clients can follow those URLs without constructing them. Stripe explicitly does not use HATEOAS: every endpoint is a documented URL template like POST /v1/charges and clients construct paths from the API reference. Stripe relies on versioning (the Stripe-Version header pinning a date) plus careful additive evolution for backward compatibility. Both approaches work at scale. The lesson is that HATEOAS is most valuable when the resource lifecycle is complex (many states with different legal transitions) or when the API spans multiple producers; for a CRUD-shaped payment API with one producer, URL templates plus a version header are simpler and equally durable.
How do clients consume HATEOAS APIs?
A HATEOAS client starts at a single entry point — usually a root URL like /api — fetches the root response, and then follows _links by relation name (rel) to reach every other resource. Instead of code like fetch(/orders/{id}), the client does fetch(root._links.orders.href + /{id}), or better, follows the orders relation from a customer resource the server already returned. Most client libraries (Spring HATEOAS Traverson, hal-client in Python, ketting in JavaScript) provide chainable traversal: traverson.from(root).follow("orders", "self").get(). The benefit is that URLs become server-controlled — moving /orders to /v2/orders requires no client change. The cost is one extra round-trip on first use (the root fetch) and the discipline to never construct URLs in client code. Caching the root response and the link relations table eliminates the round-trip overhead in steady state.
Does GraphQL replace HATEOAS?
GraphQL solves a different problem. HATEOAS lets the server tell the client which actions are legal right now and where to find related resources. GraphQL lets the client tell the server which fields it wants and traverse relationships in a single round-trip. Neither covers both. A GraphQL schema is a static contract — the client knows from the schema what queries and mutations exist, and there is no runtime mechanism for the server to say "the close-account mutation is not available on this account because the balance is non-zero." You can model that as a field on the type (canClose: Boolean), which is essentially HATEOAS for GraphQL. In practice, teams that want hypermedia inside GraphQL add explicit availability flags or action lists to their types. The two approaches are complementary, not substitutes — HATEOAS is about state transitions and link discovery; GraphQL is about query shape and over/under-fetching.
Why did HATEOAS not become mainstream?
Three reasons. First, the dominant client of most APIs is a single-producer team writing both the server and the client — they coordinate URL changes directly, so the decoupling HATEOAS offers has no payoff. Second, documentation tooling (Swagger, OpenAPI) made URL-templated APIs trivially discoverable through a UI, removing one of the original motivations (runtime discovery). Third, generic HATEOAS clients never materialized in practice — every team writes their own client, often parsing the JSON shape directly rather than walking links, so the server gains nothing by emitting links the client ignores. HATEOAS still wins in three niches: long-lived public APIs with many independent clients, federated systems where multiple producers expose interlinked resources, and APIs where the legal action set varies heavily by resource state. Outside those niches, URL templates plus OpenAPI plus versioning win on simplicity.
How does HATEOAS interact with OpenAPI/Swagger?
OpenAPI and HATEOAS describe different things. OpenAPI documents the entire URL surface of an API statically — every path, every method, every parameter, every schema — in a single file the client reads at build time. HATEOAS documents the legal transitions from any given resource at runtime — what links does this specific response include right now. The two coexist cleanly: an OpenAPI document can describe the response shape including the _links section, and HATEOAS clients can use the OpenAPI doc to understand link semantics (which rel names exist, what each rel returns). The OpenAPI 3.1 spec includes a links field on responses that is conceptually HATEOAS-aligned but underused in practice. If you publish both, treat OpenAPI as the contract for response shape and link semantics, and HATEOAS as the runtime mechanism for telling clients which subset of those links applies to this specific response.
Further reading and primary sources
- Roy Fielding — Architectural Styles and the Design of Network-based Software Architectures — The 2000 PhD dissertation that defined REST, including the HATEOAS constraint in Section 5.1.5
- Roy Fielding — REST APIs must be hypertext-driven — The 2008 blog post that crystallized Fielding's position on what does and does not qualify as REST
- HAL Specification (Mike Kelly Internet-Draft) — The authoritative HAL spec — _links, _embedded, CURIEs, and the application/hal+json media type
- Siren Specification — Kevin Swiber's Siren spec — class, properties, entities, links, actions for application/vnd.siren+json
- JSON:API Specification — JSON:API 1.1 spec including links, relationships, and the included sub-resource pattern
- Martin Fowler — Richardson Maturity Model — The widely cited explainer of the four-level maturity model for REST APIs
- Spring HATEOAS Reference — The most mature server-side HATEOAS toolchain — EntityModel, link builders, HAL by default