4 min read

Spring Boot HATEOAS for Hypermedia-Driven RESTful Web Service

November 12, 2020

Hypermedia is the most important aspect of RESTful services. To explain in short Hypermedia is a format where the response for one resource contains links to other resources that are related. Any system that uses this format is called Hypertext As The Engine Of Application State or HATEOAS for short.

Consider a web page as a resource. It contains links to other pages. This is what we call as HyperText pages(HTML). Now imagine if same thing can be done to a JSON response. That is what the media type: application/hal+xml and media type: application/hal+json are for.

With these new hypermedia format, a simple order object would look like below.

{
  "orderId": 1,
  "total": 54.99,
  "_links": {
    "self": {
      "href": "http://localhost:8080/orders/1"
    },
    "account": {
      "href": "http://localhost:8080/accounts/2"
    },
    "items": {
      "href": "http://localhost:8080/orders/1/items"
    }
  }
}

The response may look simple. The clients who receives these responses only have to follow the linked resources to get the information they want. But for a server to generate these references, It needs to have proper POJOs that can take multiple links. A way to reference other controller methods in the context of current request etc.

If we have to implement all of this by ourself, we are going to end up having a huge pile of code liability. Thanks to HATEOAS starter in spring boot, this job is made easy for the developers.

Let’s see how we can convert the example in our previous post into a HATEOAS compliant RESTful service.

Project setup for HATEOAS starter

The setup is really simple. In the source code of previous post, Just replace spring-boot-starter-web with spring-boot-starter-hateoas.This step is so easy because internally this starter contains just the web starter and spring-hateoas library. This library comes with helper methods and a Model for Hypertext Application Language (HAL).

Now that the maven part is done, There are few changes that we need to do to our code. Let’s recollect our Items controller. Without any changes the controller methods look like below.

    @GetMapping("/")
    List<Item> get() {
        return itemRepository.findAll();
    }
    
    @GetMapping("/{itemId}")
    Item get(@PathVariable Integer itemId) {
        return itemRepository.findById(itemId).orElseThrow(RuntimeException::new);
    }

And the responses for these controller methods are like these

GET http://localhost:8080/items
[
  {
    "id": 1,
    "itemName": "Shoe",
    "price": 12.99
  },
  {
    "id": 2,
    "itemName": "Shirt",
    "price": 8.99
  }
]
GET http://localhost:8080/items/2
{
  "id": 2,
  "itemName": "Shirt",
  "price": 8.99
}

In order to add the _links to these responses, spring-hateoas provides two major wrapper classes. They are EntityModel and CollectionModel. Using these classes we can add the related links with few lines of code. Let’s see how I have done it.

package com.springhow.examples.springboot.rest.controller;

import com.springhow.examples.springboot.rest.entities.Item;
import com.springhow.examples.springboot.rest.entities.repositories.ItemRepository;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;


@RestController
@RequestMapping("/items")
public class ItemController {

    private final ItemRepository itemRepository;

    public ItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    @GetMapping("/")
    CollectionModel<EntityModel<Item>> get() {
        List<EntityModel<Item>> items = itemRepository.findAll().stream().map(item -> EntityModel.of(item,
                linkTo(methodOn(ItemController.class).get(item.getId())).withSelfRel(),
                linkTo(methodOn(ItemController.class).get()).withRel("items")))
                .collect(Collectors.toList());
        return CollectionModel.of(items, linkTo(methodOn(ItemController.class).get()).withSelfRel());
    }

    @GetMapping("/{itemId}")
    EntityModel<Item> get(@PathVariable Integer itemId) {
        Item item = itemRepository.findById(itemId).orElseThrow(RuntimeException::new);
        return EntityModel.of(item,
                linkTo(methodOn(ItemController.class).get(itemId)).withSelfRel(),
                linkTo(methodOn(ItemController.class).get()).withRel("items"));
    }
}

Here the linkTo and methodOn are static methods from hateoas library that helps deal with generating URLs. With the above setup done, Let’s fire the APIs.

GET http://localhost:8080/items/1

{
  "id" : 1,
  "itemName" : "Shoe",
  "price" : 12.99,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/items/1"
    },
    "items" : {
      "href" : "http://localhost:8080/items/"
    }
  }
}

Take another example where once Cart is submitted, We are expecting to create an order. In this case, The cart is not directly related to the order which was created. However, We can provide a link to the created Order resource through HAL like this.

    @PutMapping("/{cartId}")
    EntityModel<Cart> update(@PathVariable Integer cartId) {
        Cart cartFromDB = cartRepository.findById(cartId).orElseThrow(RuntimeException::new);
        cartFromDB.setStatus(CartStatus.SUBMITTED);
        //Ignoring validation if the cart content has changed for simplicity.
        OrderHeader orderHeader = orderService.saveOrder(cartFromDB);
        return EntityModel.of(cartFromDB,
                linkTo(methodOn(CartController.class).get(cartFromDB.getId())).withSelfRel(),
                linkTo(methodOn(OrderController.class).getOrder(orderHeader.getId())).withRel("order"));    }

Note that the highlighted line refers to a different controller. This is definitely not possible before.

Note that the underlying service methods doesn’t change a single bit. The POSTs can still take a simple POJO for inputs as far as RESTfulness is concerned.

The full code for this implementation is available below. Feel free to clone and checkout.

In the next post we will see about setting appropriate response codes for the RESTful methods.

About the author
Raja Anbazhagan

I'm a Software Engineer with over 7 years of experience in working with Enterprise Java applications. Lately, I am focused on cloud-based Java applications and serverless technologies. I spend most of my spare time on stackoverflow and this blog.