Back to: Spring Boot Tutorials
Spring Boot HATEOAS with Examples
In this article, I am going to discuss Spring Boot HATEOAS with Examples. Please read our previous article where we discussed Spring Boot Auto Configuration and Dispatcher Servlet with Examples.
What is HATEOAS?
HATEOAS, or Hypermedia as the Engine of Application State, refers to the use of hypermedia (content that contains links to other forms of media such as images, movies, and text) in RESTful applications. This distinguishes REST from other network architectures. With HATEOAS, a client interacts with a network application by receiving dynamic information through hypermedia provided by the application server.
Spring-HATEOAS is a library of APIs that can be used to create REST representations that follow the HATEOAS principle when working with Spring MVC. The Spring HATEOAS project does not require a Servlet Context or concatenation of the path variable to the base URI. Instead, it offers three abstractions for creating URIs: ControllerLinkBuilder, Link, and ResourceSupport. These abstractions can be used to create metadata associated with the resource representation.
Features and Uses of HATEOAS
- It supports hypermedia formats like HAL.
- It provides a Link builder API to create links pointing to MVC controller methods.
- Model classes for the link, and resource representation models.
How to Implement HATEOAS in Spring Boot?
For this project, we shall use the fruits API from the previous projects. In this exercise, we shall modify the retrieve operations of the existing REST API to use HATEOAS. The project should contain the following files:
- FruitExceptionController.java
- FruitNotFoundException.java
- Fruits.java
- FruitServiceController.java
- RestfulApplication.java
In the project, we shall only be modifying Fruits.java and FruitServiceController.java. As a reference, this should be the existing content of the files:
Fruits.java
package com.dotnet.restful; public class Fruits { private String id; private String name; public String getId() {return id;} public void setId(String id) {this.id = id;} public String getName() {return name;} public void setName(String name) {this.name = name;} public Fruits (String nid, String nname) { id = nid; name = nname; } }
FruitServiceController.java
package com.dotnet.restful; //Map classes import java.util.HashMap; import java.util.Map; //Required web classes import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController public class FruitServiceController { private static Map<String, Fruits> productRepo = new HashMap<>(); static { Fruits apple = new Fruits("1","Apple"); productRepo.put(apple.getId(), apple); Fruits banana = new Fruits("2","Banana"); productRepo.put(banana.getId(), banana); Fruits chiku = new Fruits("3","Chiku"); productRepo.put(chiku.getId(), chiku); Fruits dragon = new Fruits("4","Dragon Fruit"); productRepo.put(dragon.getId(), dragon); } @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE) public ResponseEntity<Object> delete(@PathVariable("id") String id) { if (!productRepo.containsKey(id)) throw new FruitNotFoundException(); productRepo.remove(id); return new ResponseEntity<>("Fruit is deleted!", HttpStatus.OK); } @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT) public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Fruits product) { if (!productRepo.containsKey(id)) throw new FruitNotFoundException(); productRepo.remove(id); product.setId(id); productRepo.put(id, product); return new ResponseEntity<>("Fruit is updated!", HttpStatus.OK); } @RequestMapping(value = "/products", method = RequestMethod.POST) public ResponseEntity<Object> createProduct(@RequestBody Fruits product) { productRepo.put(product.getId(), product); return new ResponseEntity<>("Fruit is created!", HttpStatus.CREATED); } @RequestMapping(value = "/products") public ResponseEntity<Object> getProduct() { return new ResponseEntity<>(productRepo.values(), HttpStatus.OK); } }
Perform the following steps to implement HATEOAS in Spring Boot:
Step 1: Modify pom.xml to include the HATEOAS dependency:
Step 2: Modify the Fruits.java file to implement the RepresentationModel class. Remember to import the required package:
Step 3: Modify the FruitServiceController.java file to add a new getOne() function. This function will implement HATEOAS. The content of the function is:
This function will execute when a GET request is sent to http://localhost:8080/products/{id}, where {id} is the id of a fruit.
Step 4: Modify the getProduct() function in the FruitServiceController.java file to implement HATEOAS. The content of the function currently is:
Change it to:
Step 5: Compile and execute the application. Ensure compilation is successful:
Step 6: Open Postman and send a GET request to http://localhost:8080/products/1. The output should be:
As can be seen, a self-link is included in the result. This shows us that HATEOAS is successfully implemented.
Step 7: Open Postman and send a GET request to http://localhost:8080/products/. The output should show all the fruits:
The complete output shall be:
{ "_embedded": { "fruitsList": [ { "id": "1", "name": "Apple", "_links": { "self": { "href": "http://localhost:8080/products/1" } } }, { "id": "2", "name": "Banana", "_links": { "self": { "href": "http://localhost:8080/products/2" } } }, { "id": "3", "name": "Chiku", "_links": { "self": { "href": "http://localhost:8080/products/3" } } }, { "id": "4", "name": "Dragon Fruit", "_links": { "self": { "href": "http://localhost:8080/products/4" } } } ] }, "_links": { "self": { "href": "http://localhost:8080/products" } } }
As can be seen, a link is present to each individual fruit. Furthermore, a self-link is also present. Congratulations! You now know how to implement a REST API using HATEOAS.
The Complete Example Code
The complete code for the project is as follows:
src/main/java/com/dotnet/restful/FruitExceptionController.java
package com.dotnet.restful; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class FruitExceptionController { @ExceptionHandler(value = FruitNotFoundException.class) public ResponseEntity<Object> exception (FruitNotFoundException e) { return new ResponseEntity<>("Fruit not found!", HttpStatus.NOT_FOUND); } }
src/main/java/com/dotnet/restful/FruitNotFoundException.java
package com.dotnet.restful; public class FruitNotFoundException extends RuntimeException { }
src/main/java/com/dotnet/restful/Fruits.java
package com.dotnet.restful; import org.springframework.hateoas.RepresentationModel; public class Fruits extends RepresentationModel<Fruits> { private String id; private String name; public String getId() {return id;} public void setId(String id) {this.id = id;} public String getName() {return name;} public void setName(String name) {this.name = name;} public Fruits (String nid, String nname) { id = nid; name = nname; } }
src/main/java/com/dotnet/restful/FruitServiceController.java
package com.dotnet.restful; import java.util.Collection; //Map classes import java.util.HashMap; import java.util.Map; import org.springframework.hateoas.CollectionModel; import org.springframework.http.HttpEntity; //Required web classes import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @RestController public class FruitServiceController { private static Map<String, Fruits> productRepo = new HashMap<>(); static { Fruits apple = new Fruits("1","Apple"); productRepo.put(apple.getId(), apple); Fruits banana = new Fruits("2","Banana"); productRepo.put(banana.getId(), banana); Fruits chiku = new Fruits("3","Chiku"); productRepo.put(chiku.getId(), chiku); Fruits dragon = new Fruits("4","Dragon Fruit"); productRepo.put(dragon.getId(), dragon); } @GetMapping (value = "/products/{id}") public HttpEntity<Fruits> getOne(@PathVariable("id") String id) { Fruits f = productRepo.get(id); f.add(linkTo(methodOn(FruitServiceController.class).getOne(id)).withSelfRel()); return new ResponseEntity<Fruits>(f, HttpStatus.OK); } @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE) public ResponseEntity<Object> delete(@PathVariable("id") String id) { if (!productRepo.containsKey(id)) throw new FruitNotFoundException(); productRepo.remove(id); return new ResponseEntity<>("Fruit is deleted!", HttpStatus.OK); } @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT) public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Fruits product) { if (!productRepo.containsKey(id)) throw new FruitNotFoundException(); productRepo.remove(id); product.setId(id); productRepo.put(id, product); return new ResponseEntity<>("Fruit is updated!", HttpStatus.OK); } @RequestMapping(value = "/products", method = RequestMethod.POST) public ResponseEntity<Object> createProduct(@RequestBody Fruits product) { productRepo.put(product.getId(), product); return new ResponseEntity<>("Fruit is created!", HttpStatus.CREATED); } @GetMapping(value = "/products") public CollectionModel<Fruits> getProduct() { Collection<Fruits> fruits = productRepo.values(); for (Fruits f : fruits) f.add(linkTo(methodOn(FruitServiceController.class).getOne(f.getId())).withSelfRel()); CollectionModel<Fruits> collectionModel = CollectionModel.of(fruits); collectionModel.add(linkTo(methodOn(FruitServiceController.class).getProduct()).withSelfRel()); return collectionModel; } }
src/main/java/com/dotnet/restful/RestfulApplication.java
package com.dotnet.restful; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RestfulApplication { public static void main(String[] args) { SpringApplication.run(RestfulApplication.class, args); } }
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.6</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.dotnet</groupId> <artifactId>restful</artifactId> <version>0.0.1-SNAPSHOT</version> <name>restful</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
In the next article, I am going to discuss Spring Boot RESTful Content Negotiation. Here, in this article, I try to explain Spring Boot HATEOAS with Examples. I hope you enjoy this Spring Boot HATEOAS article.