← Retour au blog
backendapirest

Building Truly RESTful APIs with HATEOAS

·8 min de lecture

REST is the most widely used architectural style for APIs, but it's frequently misunderstood. It's not a protocol or a standard — it's a set of architectural constraints. Following some of them gives you a RESTful-like API. Following all of them gives you a truly RESTful one.

The constraint that gets dropped most often is HATEOAS. Most crash courses don't mention it because it adds implementation complexity. That omission leaves a lot of developers unaware it exists until they're deep in a project and starting to feel friction they can't name.

The six REST constraints

Before HATEOAS specifically, here's the full picture:

1. Client-server — the client and server are separated and evolve independently. The client knows resource URIs; the server handles storage and business logic. Neither should care about the other's implementation.

2. Stateless — the server stores no session state about the client. Every request contains all information needed to process it. This is what makes REST services horizontally scalable — any server can handle any request.

3. Cacheable — responses must declare whether they're cacheable. Clients, proxies, and CDNs can all cache responses, reducing load and latency. HTTP's Cache-Control, ETag, and Last-Modified headers are the mechanism.

4. Layered system — the client doesn't know whether it's talking directly to the origin server or through load balancers, API gateways, or reverse proxies. Each layer only knows about adjacent layers.

5. Code on demand (optional) — servers can send executable code to the client (JavaScript, for example). The only optional constraint in the list.

6. Uniform interface — the interface between client and server must be standardized. This is REST's most important constraint, and HATEOAS is part of it.

The uniform interface, unpacked

The uniform interface has four sub-constraints:

Resource identification in requests — resources are identified by URIs. Use plural nouns, not verbs. /users/159 not /getUser?id=159. Consistency matters more than any particular convention — using /users for a collection and /customers/159 for an individual is a violation.

Manipulation through representations — clients read and modify resources via representations (JSON, XML). The representation can differ from the server's internal storage format. A User object in the database can be returned as JSON, modified via a PUT with JSON, and the server translates both ways.

Self-descriptive messages — each message carries enough information to describe how to process it. HTTP methods (GET, POST, PUT, DELETE, PATCH) describe intent. HTTP status codes describe outcomes (200 OK, 404 Not Found, 409 Conflict). Clients shouldn't need out-of-band knowledge to interpret a response.

HATEOAS — the one everyone skips.

What HATEOAS actually means

Hypermedia As The Engine Of Application State.

The idea: a resource's response should include links to related actions and resources, so clients can discover what they can do next without prior knowledge baked in.

Consider GET /books/123 without HATEOAS:

{
  "id": 123,
  "title": "Clean Code",
  "author": "Robert Cecil Martin",
  "reviews": [
    { "user": "Aymane", "rating": 5, "comment": "Wonderful book." },
    { "user": "Sam", "rating": 3, "comment": "I like it." }
  ]
}

Two problems: the response embeds reviews inline (which blows up for popular books), and there's no URI for reviews — so how does the client fetch just one? It can't, unless you hardcode /books/123/reviews in the client.

With HATEOAS:

{
  "id": 123,
  "title": "Clean Code",
  "author": "Robert Cecil Martin",
  "links": {
    "self": "/books/123",
    "reviews": "/books/123/reviews",
    "author": "/authors/42"
  }
}

The client doesn't need to know the URL structure for reviews. The server tells it. This is the core shift: the server drives navigation, not the client.

Why this matters in practice

Decoupling — the server can change its URL structure without breaking clients. If /books/123/reviews becomes /reviews?book=123, clients that follow links rather than hardcode paths adapt automatically on the next response.

Discoverability — a client can explore the API by following links, the same way a browser navigates the web. This is what "the web is RESTful" actually means — browsers don't hardcode every URL, they follow hyperlinks.

Self-documentation — the API tells you what's possible at any given state. A link to a delete action only appears if the user has permission to delete. The client doesn't need to know the permission rules; it just checks whether the link is present.

Implementation: established hypermedia formats

Rather than inventing your own link format, use an established one. Two common choices:

HAL (Hypertext Application Language)

{
  "_links": {
    "self": { "href": "/posts/123" },
    "update": { "href": "/posts/123" },
    "delete": { "href": "/posts/123" },
    "author": { "href": "/authors/1" }
  },
  "id": 123,
  "title": "RESTful API Design",
  "content": "..."
}

Siren

{
  "class": ["post"],
  "properties": {
    "id": 123,
    "title": "RESTful API Design"
  },
  "links": [
    { "rel": ["self"],   "href": "/posts/123" },
    { "rel": ["author"], "href": "/authors/1" }
  ],
  "actions": [
    {
      "name": "delete-post",
      "method": "DELETE",
      "href": "/posts/123"
    }
  ]
}

Siren's actions block is more expressive — it includes the HTTP method and can describe request fields, making the API fully self-describing. HAL is simpler and more widely adopted. Which to choose depends on how much client discovery you actually need.

For Java/Spring applications, Spring HATEOAS handles link generation automatically. For Node.js, hal is a lightweight option.

Practical guidelines

Generate links dynamically — never hardcode URIs in your responses. Use routing utilities or helper functions to generate them based on the current resource and request context. Hardcoded links break the moment you change your routing structure.

Use standard relation typesself, next, prev, first, last for pagination; update, delete for mutations. These are defined in the IANA link relations registry and understood by generic clients.

Link based on state — only include links for actions that are currently valid. If a resource is in a locked state, don't include the update link. The presence or absence of a link communicates permitted operations.

Lower your TTL on change — when adding HATEOAS to an existing API, reduce cache TTLs first. Old clients with cached responses that don't contain links won't break, but you want new responses propagating quickly.

Degrees of REST maturity

Leonard Richardson's maturity model is a useful frame:

Most APIs marketed as RESTful are at level 2. Level 3 is what Fielding's original dissertation actually describes. The distinction matters when you're building APIs meant to evolve — a level 2 API couples clients to its URL structure; a level 3 API doesn't.

REST constraints exist because each one solves a real problem at scale. You can ignore HATEOAS in an internal API where you control both sides and can coordinate changes. But ignoring it while calling the result "truly RESTful" is just wrong — and understanding why it exists helps you decide consciously when the tradeoff is worth making.