mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Tuesday, 1 December, 2020

HATEOAS without links

Yes, I know this title sounds stupid, but could not find something that fits better. So let me explain why I think that links in HATEOAS APIs are not always that useful.

If you don't know what HATEOAS is, I recommend reading my Introduction to Hypermedia REST APIs first.

REST APIs with HATEOAS support provide two main features for decoupling client and server:

  1. Hypermedia avoids that the client needs to hard-code and construct URIs. This helps the server to evolve the REST-API in the future.
  2. The availability of links tells the client which operations can be performed on a resource. This avoids that server logic needs to be duplicated on the client.
    For example, assume the client needs to decide if a payment button should be displayed next to an order. The logic for this might be:
    if (order.status == OPEN and order.paymentDate == null) {
        show payment button
    }
    With HATEOAS the client needs not to know this logic. The check simply becomes:
    if (order.links.getByRel("payment") != null) {
        show payment button
    }
    The server can now change the rule that decides when an order can be paid without requiring a client update.

 

How useful these features are depends on your application, your system architecture and your clients.

The second point might not be a big deal for applications that mostly use CRUD operations. However, it can be very useful if your REST API is serving a more complex domain.

The first point depends on your clients and to a certain degree on your overall system architecture. If you provide an API for public clients it is very likely that at least some clients will hard-code request URIs and not use the links you provide. In this case, you loose the ability to evolve your API without breaking (at least some) clients.

If your clients do not use your API responses directly and instead expose their own API it is also unlikely that they will follow the links you return. For example, this can easily happen when using the Backend for Frontend pattern.

Consider the following example system architecture:

bff-system-architecture

A Backend Service is used by two other systems. Both systems provide user-interfaces which communicate with system specific backends. REST is used for all communication.

Assume a user performs an action using the Android-App (1). The App sends a request to the Mobile-Backend (2). Then, the Mobile-Backend might communicate with the Backend-Service (3) to perform the requested action. The Mobile-Backend can also pre-process, map or aggregate data retrieved from the Backend-Service before sending a response back to the Anroid-App.

Now back to HATEOAS.

If the Backend-Service (3) in this example architecture provides a Hypermedia REST API, clients can barely make use of HATEOAS related links.

Let's look at a sequence diagram showing the system communication to see the problem:

bff-communication-example

The Backend-Service (3) provides an API-Entrypoint which returns a list of all available operations with their request URIs. The Mobile-Backend (2) sends a request to this API-Entrypoint in regular intervals and caches the link list locally.

Now assume a user of the Android-App (1) wants to access a specific order. To retrieve the required information the Anroid-App sends a request to the Mobile-Backend (2). The URI for this request might have been retrieved from the Mobile-Backends API-Entrypoint previously (not shown).

To retrieve the requested order from the Backend-Service the Mobile-Backend uses the order-details link from the cached link list. The Backend-Service returns a response with HATEOAS links. Here, the order-payment link indicates that the order can be paid. The Mobile-Backend now transforms the response to its own return format and sends it back to the Android-App.

The Mobile-Backend might also return a HATEOAS response. So link URIs from the Backend-Service need to be mapped to the appropriate Mobile-Backend URIs. Therefore the Mobile-Backend checks if an order-payment link is present in the Backend-Service response. If this is the case it adds an order-payment link to its own response.

Note the Mobile-Backend is only using the relations (rel fields) of the Backend-Service response. The URIs are discarded.

Now the user wants to pay the order. The Android-App uses the previously retrieved order-payment link to send a request to the Mobile-Backend. The Mobile-Backend now has lost the Context of the previous Backend-Service response. So it has to look up the order-payment link in the cached link list. The process continues in the same way as the previous request

In this example the Android-App is able to make use of HATEOAS related links. However, the Mobile-Backend cannot use the link URIs returned by Backend-Service responses (except for the API entry-point). If the Mobile-Backend is providing HATEOAS features the link relations from the Backend-Service might be useful. The URIs for Backend-Service requests are always looked up from the cached API-Entrypoint response.

Communicate actions instead of links

Unfortunately link construction is not always that simple and can take some extra time. This time is wasted if you know that your clients won't use these links.

Probably the easiest way to avoid logic duplication on the client is to ignore links and use a simple actions array in REST responses:

GET /orders/123
{
    "id": 123,
    "price": "$41.24 USD"
    "status": "open",
    "paymentDate": null,
    "items": [
        ...
    ]
    "actions": ["order-cancel", "order-payment", "order-update"]
}

This way we can communicate possible actions without the need of constructing links. In this case the response tells us that the client is able to perform cancel, payment and update operations.

Note that this might not even increase coupling between the client and the server. Clients can still look up URIs for those actions in the API entry point without the need of hard-coding URIs.

An alternative is to use standard link elements and just skip the href attribute:

GET /orders/123
{
    "id": 123,
    "price": "$41.24 USD"
    "status": "open",
    "paymentDate": null,
    "items": [
        ...
    ]
    "links": [
        { "rel": "order-cancel" },
        { "rel": "order-payment" },
        { "rel": "order-update" },
    ]
}

However, it might be a bit confusing to return a links element without links URIs.

Obviously, you are leaving the standard path with both described ways. On the other side, if you don't need links you probably don't want to use a standardized HATEOAS response format (like HAL) either.

 

Leave a reply