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.
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.