🏠 ❯ RESTFul ❯ Spring Boot HATEOAS for RESTFul Web Services

Spring Boot HATEOAS for RESTFul Web Services

In this article, We will learn how to implement hateoas in a Spring boot restful web application.

Introduction to HATEOAS

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.

Spring Boot Hateoas Logo

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"
    }
  }
}Code language: JSON / JSON with Comments (json)

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.

Spring Boot 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);
    }Code language: CSS (css)

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
  }
]Code language: JavaScript (javascript)
GET http://localhost:8080/items/2
{
  "id": 2,
  "itemName": "Shirt",
  "price": 8.99
}Code language: JavaScript (javascript)

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"));
    }
}Code language: JavaScript (javascript)

Here the linkTo and methodOn are static methods from the hateoas library that help 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/"
    }
  }
}Code language: JavaScript (javascript)

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"));    
}Code language: JavaScript (javascript)

Note that the highlighted linkTo methods refer to a different controller. Previously, this was not possible. the hateoas packages from spring boot starter for hateoas help solve these issues.

Also, note that the underlying service methods don’t change at all. The POSTs can still take a simple POJO for inputs as far as RESTfulness is concerned.

To Conclude, we learned how to create hateoas compliant restful web services using Spring Boot starter for hateoas. The full code for this implementation is available below. Feel free to clone and checkout.

Related

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *