HATEOAS, Hypermedia and the Richardson Maturity Model

Intermediate
11 min

HATEOAS, Hypermedia and the Richardson Maturity Model

Most APIs that call themselves "RESTful" stop at clean URLs and correct HTTP methods. The original definition of REST goes one step further: responses carry links that tell the client what it can do next, just as a web page carries links and forms. In this lesson you will learn the Richardson Maturity Model, what HATEOAS means, the common link formats, and how to add hypermedia to an Express API pragmatically.

The Richardson Maturity Model

Leonard Richardson described four levels of "REST-ness". They are a useful vocabulary for reviewing any API:

| Level | Name | What it looks like | |---|---|---| | 0 | The swamp of POX | One URL, one method, action named in the body: POST /api with { "action": "getOrder" } | | 1 | Resources | Separate URLs per thing: /orders/42, /customers/7, but every call is still POST | | 2 | HTTP verbs and status codes | GET, POST, PUT, DELETE used with their real meaning; 201, 404, 409 and friends | | 3 | Hypermedia controls | Responses include links to related resources and available actions |

Most production APIs sit at level 2, which is a good place to be. Level 3 is what HATEOAS adds.

What HATEOAS Means

HATEOAS stands for Hypermedia As The Engine Of Application State. A client should not hard-code every URL and business rule; it fetches a resource, reads the links in the response, and follows them. Which links are present depends on the resource's current state.

Consider an order. Whether it can be cancelled depends on whether it has shipped. Without hypermedia, every client re-implements that rule and breaks when it changes. With hypermedia, the server includes a cancel link only while cancelling is allowed:

json
{ "id": "ord_42", "status": "pending", "total": 2499, "_links": { "self": { "href": "/orders/ord_42" }, "customer": { "href": "/customers/cus_7" }, "pay": { "href": "/orders/ord_42/payments", "method": "POST" }, "cancel": { "href": "/orders/ord_42", "method": "DELETE" } } }

Once the order ships, pay and cancel disappear and a track link appears. The client renders buttons for the links it finds.

Link Formats

There is no single mandated format. Three conventions cover almost everything you will meet:

  • HAL (application/hal+json): a _links object keyed by relation name, plus _embedded for included related resources. The example above is HAL-style.
  • JSON:API (application/vnd.api+json): a links object and a strict envelope of data, relationships and included.
  • The Link header (RFC 8288): links outside the body, ideal for pagination because the body stays a plain array.
bash
curl -i "https://api.example.com/orders?page=3" | grep -i '^link:' # Link: </orders?page=4>; rel="next", </orders?page=2>; rel="prev", </orders?page=9>; rel="last"

Relation names such as self, next, prev and collection are registered with IANA; invent names like cancel only when no standard one fits.

Implementing Links in Express

Keep link construction in one function per resource so the rules live in a single place:

javascript
function orderLinks(order) { const base = `/orders/${order.id}`; const links = { self: { href: base }, customer: { href: `/customers/${order.customerId}` }, }; if (order.status === "pending") { links.pay = { href: `${base}/payments`, method: "POST" }; links.cancel = { href: base, method: "DELETE" }; } if (order.status === "shipped") { links.track = { href: `/shipments/${order.shipmentId}` }; } return links; } app.get("/orders/:id", (req, res) => { const order = db.get(req.params.id); if (!order) return res.status(404).json({ error: "Order not found" }); res.json({ ...order, _links: orderLinks(order) }); });

Build absolute URLs from a configured base, never from the incoming Host header, which an attacker can spoof.

Tips

  • Start with the links that pay for themselves: self, pagination, and state-dependent actions.
  • Links are not authorization. Omit a link the caller may not use, but still enforce the rule on the endpoint.
Quick Quiz
Question 1 of 2

At which Richardson level does an API use `GET`, `POST`, `PUT` and `DELETE` with proper status codes but return no links?

Key Takeaways

  • The Richardson Maturity Model ranks APIs from a single RPC endpoint (level 0) to full hypermedia (level 3).
  • HATEOAS means responses carry links describing related resources and the actions currently available.
  • Common formats are HAL _links, JSON:API links, and the RFC 8288 Link header.
  • Use hypermedia pragmatically: self, pagination and state-dependent actions deliver most of the value.

Next lesson: Testing APIs with Postman and curl — send real requests to your endpoints and inspect every header and status code.

HATEOAS, Hypermedia and the Richardson Maturity Model - REST APIs | CodeYourCraft | CodeYourCraft