Boris Eetgerink

November 18, 2022

REST and Hypermedia as the engine of application state

My previous post REST for object oriented software developers only scratched the surface of REST. It was meant as an introduction on how to think about the states and state transitions of resources. This post is more of a deep dive into REST and HATEOAS. But before we get there, we have to start with media types (formerly known as MIME types) and content negotiation.

Media types and content negotiation

One of the requirements of REST is that the client knows nothing about the server beforehand. There may be no contracts or Swagger-like API documentation. You can compare this with a web browser opening a site. The browser knows nothing about it before it has loaded the first page. But despite the browser knowing nothing about a site in advance, it can render it just fine. The reason for this is that it understands the HTML media type (and a lot of other media types for that matter).

For a browser's request to a server to be successful, both have to agree on the media type to transfer. That's called content negotiation and is part of the HTTP specification. It's actually done for every HTTP request. The browser advertises the media types it supports with the Accept header, like so:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

In this example, the browser prefers the HTML or XHTML media types, but XML is fine too. If those are not available, it is OK with any media type the server has to offer. The reason the browser has to be so flexible is exactly because it doesn't know anything about the response in advance. The server might return HTML, an image, plain text, a style sheet, a spreadsheet or something else entirely.

The server in response determines which of the requested media types it supports and returns the requested content in the media type the browser prefers best. The server uses the Content-Type header to indicate which media type it chose, like so:

Content-Type: text/html; charset=utf-8

In this example the server has the content in the HTML media type available, so it returns the HTML with a media type parameter indicating the use of the UTF-8 encoding.

Hypermedia capable media types

As Roy Fielding points out in REST APIs must be hypertext-driven, there is a distinction between media types that support hypertext and those that don't. Hypertext (or hypermedia, there is not a clear distinction) is text that allows linking (with hyperlinks) between resources. The most common example of a hyperlink is the anchor element in HTML.

It is clear that HTML (HyperText Markup Language) supports hyperlinks and so is a hypermedia capable media type. However, most REST APIs use JSON (JavaScript Object Notation) as the media type to represent resources and that's a problem, because JSON itself is not a hypermedia capable media type. It does not have the notion of hyperlinks as HTML does.

To make JSON hypermedia capable, a few media types have been introduced that build on top of JSON. This blog post from 2014 is a good summary of what's available. Not much has changed since then, with the exception that JSON:API has matured and that the Ion Hypermedia Type is introduced.

Hypermedia as the engine of application state (HATEOAS)

As stated earlier, in REST, a client may know nothing about a server beforehand. The client must start at the root URL (/) and explore the available options, just like when browsing a website. A hypermedia capable media type should be used to expose the available options to the client. Unfortunately, Roy Fielding's dissertation is very abstract and doesn't provide pointers on how to implement this. Fortunately, there is a lot of experience in the industry, so I can come up with a few examples. I'll use a blogging system as an example from my previous post again and use the Ion media type.

First, the client makes a request to the root of the API:

GET / HTTP/1.1
Accept: application/ion+json

And the server responds:

HTTP/1.1 200 OK
Content-Type: application/ion+json
{
    "authors": {
        "href": "https://example.com/authors",
        "rel": ["collection"],
        "title": "Authors"
    },
    "posts": {
        "href": "https://example.com/posts",
        "rel": ["collection"],
        "title": "Blog posts"
    },
    "value": []
}

In the response the server advertises that the root resource itself is an empty collection and that it has a link to the authors and posts collections. For a more realistic API, it would include a link object for every resource collection below the root.

The client can now follow the posts link:

GET /posts HTTP/1.1
Accept: application/ion+json

Now the response from the server is more elaborate. I include only one post in the collection for brevity:

HTTP/1.1 200 OK
Content-Type: application/ion+json
{
    "self": {
        "href": "https://example.com/posts",
        "rel": ["collection", "self"]
    },
    "first": {
        "href": "https://example.com/posts",
        "rel": ["collection", "first"]
    },
    "prev": null,
    "next": {
        "href": "https://example.com/posts?page=2",
        "rel": ["collection", "next"]
    },
    "last": {
        "href": "https://example.com/posts?page=5",
        "rel": ["collection", "last"]
    },
    "value": [
        {
            "self": {
                "href": "https://example.com/posts/1",
                "rel": ["self"]
            },
            "author": {
                "href": "https://example.com/authors/42",
                "rel": ["author"]
            },
            "title": "Hello world",
            "summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "publicationDate": "2022-11-18"
        }
    ]
}

The client can now choose the follow the pagination links or follow the link to a specific blog post. The client knows there are pagination links in the response, because of the established link relations (first, prev, next and last).

This is an example request for the blog post:

GET /posts/1 HTTP/1.1
Accept: application/ion+json

And the server responds with:

HTTP/1.1 200 OK
Content-Type: application/ion+json
{
    "self": {
        "href": "https://example.com/posts/1",
        "rel": ["self"]
    },
    "collection": {
        "href": "https://example.com/posts",
        "rel": ["collection", "up"]
    },
    "author": {
        "href": "https://example.com/authors/42",
        "rel": ["author"]
    },
    "delete": {
        "href": "https://example.com/posts/1",
        "method": "DELETE",
        "title": "Delete post",
        "desc": "Permanently delete this blog post."
    },
    "value": {
        "title": "Hello world",
        "publicationDate": "2022-11-18",
        "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ante, tincidunt non tempor sit amet, feugiat dapibus nulla..."
    }
}

Now the client has all the details of the post and the workflow actions available to the user. It can display the post and show a delete button. It can get the author details by following the author link.

Summary

There are a few advantages to applying HATEOAS:

  • There might be more work in making the client flexible and making it understand a hypermedia capable media type, but on the other hand less work is required in maintaining documentation and API contracts.
  • The work put into making the client understand a hypermedia capable media type can be applied to other projects as well, while work put into maintaining documentation and API contracts is much harder to transfer between projects.
  • The server is flexible in the URLs it exposes. A change in the URL structure doesn't break a client, because the client explores the available options, starting from the root URL.
  • The server can 'program' the client without requiring changes in the client code as long as the client understands the media type.

About Boris Eetgerink

Hey, this is my C# .NET blog. I don't have a comments system, but you can contact me at boriseetgerink at hey dot com.