How to incorporate HATEOAS resource representation into a RESTful Spring Boot application

To be effective, a REST application must do more than simply process data. A quality REST application should steer the client to relevant resources and help the client get the most out of each interaction. To facilitate interactions with an application's resources, we can incorporate Spring HATEOAS (HATEOAS stands for Hypermedia as the Engine of Application State). Spring HATEOAS is an API library designed to make it easier for a client to interact with a REST application, even if the client has little knowledge of how the application is organised. Spring HATEOAS facilitates client interactions by supplementing application responses with hypermedia. For example, Spring HATEOAS allows hyperlinks to related application resources to be included with responses. The client can use the links to navigate around the application's resources and accomplish its needs, even without prior knowledge of the application's structure. Providing real-time links also eliminates the need to hard code information about the application's structure because the client can always access up-to-date information by querying the application. In this way, Spring HATEOAS minimises the disruption to the client if the structure of the application changes. Also, Spring HATEOAS improves the discoverability of the application because the client can learn how to interact with the application based on the links that are included in the responses.

spring-hateoas-rest-links.png

The full code for the project covered in this tutorial can be found on our Github.

Configure Spring HATEOAS

To configure a Spring Boot application to support Spring HATEOAS, either include the Spring HATEOAS dependency when preparing the project using the Spring Initializr console or add the dependency manually to an existing project. Dependencies can be added manually to an existing project by opening the project's pom.xml file, which can be found in the project's root directory.

spring-boot-maven-pom.png

The pom.xml file coordinates the dependencies for the projects that are built using Maven. Locate the dependencies element and add the Spring HATEOAS dependency as shown below:

<dependencies>
	<!-- Any existing dependencies -->

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-hateoas</artifactId>
	</dependency>
</dependencies>

Whenever you modify the pom.xml file, you should press the M icon that appears in the IntelliJ window to rebuild the project and incorporate any changes.

build-maven-project.png

Responding to HTTP REST requests using HATEOAS

Once the InventoryItemAssembler class is set up, we can configure the InventoryRestController to incorporate the assembler and include links in the response to HTTP calls. If you would like a reminder on how the InventoryRestController class was set up, then see our previous tutorial.

First, we must configure the InventoryRestController class to support the InventoryItemAssembler. To do this, replace the InventoryRestController class's primary constructor with the following code:

InventoryItemAssembler inventoryItemAssembler;

public InventoryRestController(InventoryService inventoryService, InventoryItemAssembler inventoryItemAssembler) {
    this.inventoryService = inventoryService;
    this.inventoryItemAssembler = inventoryItemAssembler;
}

Now, when the InventoryRestController is created, it will automatically initialise the InventoryItemAssembler assembler.

The first methods we will modify will be those that handle GET-based HTTP requests. GET-based requests are used to fetch information from the application. Locate the getInventory method and replace it with the following code:

@GetMapping("/items")
public ResponseEntity<CollectionModel<EntityModel<InventoryItem>>> getInventory() {
    List<InventoryItem> items = inventoryService.getAllInventoryItems();
    CollectionModel<EntityModel<InventoryItem>> collectionModel = inventoryItemAssembler.toCollectionModel(items);

    return ResponseEntity.ok()
            .body(collectionModel);
}

The above code uses the InventoryService service's getAllInventoryItems method to retrieve the full list of InventoryItem objects held by the application. Next, the list is converted to a CollectionModel using the InventoryItemAssembler assembler. Finally, the CollectionModel and a status code of 200 (OK) are returned to the client as a ResponseEntity.

In the last tutorial on creating a REST controller, we described how to use cURL commands to send HTTP requests to the application. If you test the cURL request for retrieving all the InventoryItem objects again now, you should see the collection itself and each constituent item contain links, as shown below:

curl --request GET http://localhost:8081/api/inventory/items
				
{"_embedded":{"inventoryItemList":[{"inventory_item_id":1,"title":"2022 Mixtape","quantity":0,"_links":{"self":{"href":"http://localhost:8081/api/inventory/items/1"},
"items":{"href":"http://localhost:8081/api/inventory/items"}}},{"inventory_item_id":2,"title":"2022 Mixtape","quantity":0,"_links":{"self":{"href":"http://localhost:8081/api/inventory/items/2"},
"items":{"href":"http://localhost:8081/api/inventory/items"}}},{"inventory_item_id":3,"title":"2022 Mixtape","quantity":0,"_links":{"self":{"href":"http://localhost:8081/api/inventory/items/3"},
"items":{"href":"http://localhost:8081/api/inventory/items"}}}]},"_links":{"self":{"href":"http://localhost:8081/api/inventory/items"}}}

The links returned by Spring HATEOAS are formatted using JSON Hypertext Application Language (HAL). Each link includes a URI, which is the web address that the link leads to, and a rel, which describes the relationship of the link relative to other resources and the application. For example, the above links include a "self" rel, for links that lead back to the resource, and an "items" rel, for links that can be used to request all the items held by the application. For custom rels such as "items" you should aim to be descriptive, so the client can understand what the link is used for. Using descriptive rels also helps eliminate the need for complex documentation or prior understanding of how the application works. Clients that don't speak HAL can always ignore the links and simply refer to the other data included in the response.

The getInventoryItemByID method, which is used to retrieve individual InventoryItem objects based on their ID, can also be modified to return InventoryItem objects wrapped in an EntityModel. The getInventoryItemByID will only ever return a maximum of one item, so we should use the InventoryItemAssembler assembler's toModel method rather than toCollectionModel:

@GetMapping(value = "/items/{id}")
public ResponseEntity<EntityModel<InventoryItem>> getInventoryItemByID(@PathVariable Long id) {
    InventoryItem item = inventoryService.getInventoryItemByID(id);
    if (item == null) {
        return ResponseEntity.notFound().build();
    } else {
        EntityModel<InventoryItem> entityModel = inventoryItemAssembler.toModel(item);
        return ResponseEntity.ok()
                .body(entityModel);
    }
}

Likewise, the POST, PUT and PATCH methods described in the tutorial on creating a REST controller can also be modified to an EntityModel rather than a regular InventoryItem object. The code to do this is included below:

@PostMapping("/items")
ResponseEntity<EntityModel<InventoryItem>> addItemToInventory(@RequestBody InventoryItem item) {
    InventoryItem createdItem = inventoryService.saveInventoryItem(item);

    if (createdItem == null) {
        return ResponseEntity.notFound().build();
    } else {
        EntityModel<InventoryItem> entityModel = inventoryItemAssembler.toModel(createdItem);
        return ResponseEntity
                .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri())
                .body(entityModel);
    }
}

@PutMapping(value = "/items/{id}", consumes = "application/json")
ResponseEntity<EntityModel<InventoryItem>> updateInventoryItem(
        @RequestBody InventoryItem item, @PathVariable("id") Long id) {
    InventoryItem createdItem = inventoryService.getInventoryItemByID(id);
    if (createdItem == null) {
        return ResponseEntity.notFound().build();
    } else {
        item.setInventory_item_id(id);
        inventoryService.saveInventoryItem(item);
        EntityModel<InventoryItem> entityModel = inventoryItemAssembler.toModel(item);
        return ResponseEntity.ok()
                .body(entityModel);
    }
}

@PatchMapping("/items/{id}")
public ResponseEntity<EntityModel<InventoryItem>> updateItemQuantity(
        @RequestBody Map<String, Object> payload, @PathVariable("id") Long id) {
    InventoryItem item = inventoryService.getInventoryItemByID(id);
    if (item == null) {
        return ResponseEntity.notFound().build();
    } else {
        Object newQuantity = payload.get("quantity");
        if (newQuantity instanceof Integer) {
            item.setQuantity((int) newQuantity);
            inventoryService.saveInventoryItem(item);
            EntityModel<InventoryItem> entityModel = inventoryItemAssembler.toModel(item);
            return ResponseEntity.ok()
                    .body(entityModel);
        } else {
            return ResponseEntity.badRequest().build();
        }
    }
}

The InventoryRestController class is now equipped to return relevant links whenever InventoryItem objects are created, retrieved or updated. As your application develops, you could experiment by including other useful links in the response. For example, it could be useful for the client to know where to submit PUT requests for updating the item, even if the link is the same as for the GET request.

Maintaining a reliable REST application is essential to providing a good experience for your users. If you ever need to update the schema for a link, it is important to keep the rels the same to ensure the client can continue to interact with the application uninhibited. For example, currently, each InventoryItem includes a link that runs the InventoryRestController class's getInventory method. The link is stored under the rel "items". If you need to change the structure of the link, you should continue to use the same rel. Otherwise, the client may be unaware that the link required to access the getInventory method has changed, and this could disrupt future interactions with the application.

<<< Previous

Next >>>