As part of the REST with Spring Series, this post is going to cover Spring HATEOAS. We are going to talk about the HATEOAS in REST driven APIs.
Introduction
HATEOAS (Hypermedia as the Engine of Application State) is a constraint of the REST application architecture. A hypermedia driven REST API provides information to help to navigate through the API dynamically. This is done by passing hypermedia links with the responses.HATEOAS is a fundamental concept to create Discoverable REST APIs.To get a better clarity, let’s take a look at the sample response.
{
"productId": "123",
"productName": "N3 Running Shoes",
"price":"170.00",
"description": "Pearlizumi Running shoes",
"links": [{
"rel": "self",
"href": "https://javadevjournal.com/v1/products/123"
}, {
"rel": "description",
"href": "https://javadevjournal.com/v1/products/123/description"
}]
}
In the above example, API response contains few links used by the API client for navigation. There are two important points in the links section. Let’s take a closer looks at these two points.
- rel – This indicates the relations. First (“self”) indicates a self-referencing hyperlink. The second one indicates a more complex relation.
- href – URL that uniquely defines the resource.
1. Spring HATEOAS
Spring HATEOAS provides a set of API and tools for creating REST APIs that follow the HATEOAS principle. Here are some of the features provides by Spring HATEOAS.
- Support for link and resource representation model.
- Link builder API.
- Support for HAL.
The basic principle of HATEOAS is to help the client navigate through the API by returning the relevant information as the links in the response. Take a look at above example where response contains links for the project description. In this article, we are going to create Spring HATEOAS driven REST API based on Spring Boot.
2. HATEOAS and RESTful Web Services
Hypermedia is a very important part of REST architecture. This approach allows the REST client and API to evolve independently without breaking it. As part of the design, REST API returns data along with the links to the related resources. This helps the client to navigate the API without building the URL’s (which can break if API structure change ). Let’s take a closer look at some of the benefits of using the HATEOAS while working on the REST API.
- Client API can simply follow the URL instead of building them.
- REST API server creates the URL. This has some benefits as server got all the details of the available resources.
- Inline documentation
3. Maven
In order to enable support for Spring HATEOAS, we need to add “spring-boot-starter-hateoas” in the pom.xml file.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<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>
</dependencies>
4. Example Resource
To better explain the Spring HATEOAS and REST API, let’s create our Product Resources. We are going to build hypermedia URL’s based on this resource.
public class Product {
private String code;
private String name;
private String description;
private double price;
//getter and setter
}
With no HATEOAS support, for a simple REST API, the client API will receive the following response.
{
"code": "228781",
"name": "Sample Product",
"description": "This is our first sample product",
"price": 100
}
There is no issue in the above response but if client API needs to create the URL’s, API needs to find out details about the host, context etc to build the correct URL. This can become more complicated if there is a change happened to the API structure (Client API need to rewrite the URL building logic).To handle such use cases, it is really critical and important to add hypermedia support in your REST API.
5. Enable Spring HATEOAS
To enable Spring HATEOAS support for our resource, we are going to use the base class ResourceSupport
available as part of the Spring support for HATEOAS. This base class allows us to add instances of Link
that is useful while creating the _links
element. This is how our Product
resource looks like.
public class Product extends ResourceSupport{
private String code;
private String name;
private String description;
private double price;
//getter and setter
}
The ResourceSupport
class provides add() method for link building. Let’s see this in more detail in the next section.
5.1 Creating Links
Spring HATEOAS provides a Link object to store the metadata. There are two ways to create a link while working on the RESTful API’s. We can create a simple Link object by passing the URI in the constructor.
Link link = new Link("http://{host}:{port}/v1/products/228781");
Spring HATEOAS provides another option to build links using the ControllerLinkBuilder.This is how it looks like:
linkTo(linkToController.class).slash(other parameter)).withSelfRel()
Above approach is more flexible and provides a simplified interface to build links. Another option is to use methodOn(…)
are static methods on ControllerLinkBuilder
. This is how our simple controller looks like
@GetMapping(value = "/{productId}")
public Product getProduct(final String productId) {
Product product = productService.getProductById(productId);
product.add(linkTo(ProductController.class).slash(product.getCode()).withSelfRel());
product.add(linkTo(methodOn(ProductController.class).getProduct(productId)).withSelfRel());
return product;
}
This looks really simple but there is a lot of going on in the above method. The interesting part of the creation of links which points to our controller method. Both linkTo(…)
and methodOn(…)
are static methods on ControllerLinkBuilder that allow you to fake a method invocation on the controller. The final output is the exact URI for our method. This is how the response of our method looks like:
{
"code": "228781",
"name": "Sample Product",
"description": "This is our first sample product",
"price": 100,
"_links": {
"self": [
{
"href": "http://localhost:8080/products/228781"
},
{
"href": "http://localhost:8080/products/{productId}",
"templated": true
}
]
}
}
To add more details, let’s take a look at the few important points:
- The
linkTo()
check out the controller and find the root mapping. - The
slash()
orgetProduct()
method is for the path variable or adds the value in the mapping. withSelfRel()
add self-link.
6. Spring HATEOAS and Relations
Above example was very simple but real life use cases come with complex relationships and we want to make sure that client REST API still have the ways to navigate without creating or assuming about the resource URLs.To make this more interesting, let’s assume that we also support product review with the following details:
- Products can have a number of reviews.
- The customer should be able to go to reviews from products.
- We should be able to go to product from the reviews.
This is how our ProductReview class looks like:
public class ProductReviews extends ResourceSupport {
private String code;
private String productCode;
private double score;
//getter & setter
}
Let’s extend our product controller by adding one more method to fetch reviews for a given product:
@GetMapping(value = "/{productId}/reviews", produces = {
"application/hal+json"
})
public Resources < ProductReviews > getReviewsForProduct(@PathVariable final String productId) {
List < ProductReviews > reviews = productService.getProductReviews(productId);
for (final ProductReviews review: reviews) {
Link selfLink = linkTo(methodOn(ProductController.class)
.getProduct(productId)).withSelfRel();
review.add(selfLink);
}
Link link = linkTo(methodOn(ProductController.class)
.getReviewsForProduct(productId)).withSelfRel();
return new Resources < ProductReviews > (reviews, link);
}
There are a few important points to keep in the mind:
- We are returning Resources object from our controller. It’s a general helper to easily create a wrapper for a collection of entities.
- In the second part of the method, we are creating the hyperlink to the product reviews.
6.1 The ControllerLinkBuilder
HATEOAS API provides a powerful support to build Link pointing to the Spring MVC or REST controllers. The ControllerLinkBuilder
uses Spring’s ServletUriComponentsBuilder
under the hood to obtain the basic URI information from the current request. Let’s take a look at the previous example for better understanding:
Link link = linkTo(methodOn(ProductController.class).getReviewsForProduct(productId)).withSelfRel();
In the above example, the method methodOn(…)
creates a proxy of the controller class that is recording the method invocation and exposes it in a proxy created for the return type of the method. For more details read Link builder.
7. Running Example
In the final step, let’s see our work in action. This is how our controller looks like
@RestController
@RequestMapping(value = "/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping(produces = {
"application/hal+json"
})
public Resources < Product > getProducts() {
List < Product > products = productService.getProducts();
for (final Product product: products) {
Link selfLink = linkTo(methodOn(ProductController.class)
.getProduct(product.getCode())).withRel("product");
Link productReview = linkTo(methodOn(ProductController.class)
.getReviewsForProduct(product.getCode())).withRel("review");
product.add(productReview);
product.add(selfLink);
}
Link link = linkTo(methodOn(ProductController.class)
.getProducts()).withRel("products");
return new Resources < Product > (products, link);
}
@GetMapping(value = "/{productId}")
public Product getProduct(@PathVariable final String productId) {
Product product = productService.getProductById(productId);
product.add(linkTo(ProductController.class).slash(product.getCode()).withSelfRel());
product.add(linkTo(methodOn(ProductController.class).getReviewsForProduct(productId)).withRel("reviews"));
return product;
}
@GetMapping(value = "/{productId}/reviews", produces = {
"application/hal+json"
})
public Resources < ProductReviews > getReviewsForProduct(@PathVariable final String productId) {
List < ProductReviews > reviews = productService.getProductReviews(productId);
for (final ProductReviews review: reviews) {
Link selfLink = linkTo(methodOn(ProductController.class)
.getProduct(productId)).withSelfRel();
review.add(selfLink);
}
Link link = linkTo(methodOn(ProductController.class)
.getReviewsForProduct(productId)).withRel("reviews");
return new Resources < ProductReviews > (reviews, link);
}
}
To get all the products, just run the following URL in the browser or in the curl request:http://localhost:8080/products.This is how API respond.
{
"_embedded": {
"productList": [
{
"code": "1",
"name": "Product1",
"description": "This is a demo product number :1",
"price": 12.67,
"categoryId": "cat_1",
"_links": {
"review": {
"href": "http://localhost:8080/products/1/reviews"
},
"product": {
"href": "http://localhost:8080/products/1"
}
}
},
{
"code": "2",
"name": "Product2",
"description": "This is a demo product number :2",
"price": 25.34,
"categoryId": "cat_2",
"_links": {
"review": {
"href": "http://localhost:8080/products/2/reviews"
},
"product": {
"href": "http://localhost:8080/products/2"
}
}
}
]
},
"_links": {
"products": {
"href": "http://localhost:8080/products"
}
}
}
There are a few important points we should be looking at:
- In the response we are getting a link to the individual product as well the link to the product reviews.
- On clicking the product link, we navigate to the product details.
- To check product reviews, simply use the “review section”
As a REST client, we have al the details to navigate to the different sections without creating those URLs. If you click on the product URL, this is what you can expect:
{
"code": "1",
"name": "Sample Product",
"description": "This is our first sample product",
"price": 100,
"categoryId": "Cat1",
"_links": {
"self": {
"href": "http://localhost:8080/products/1"
},
"reviews": {
"href": "http://localhost:8080/products/1/reviews"
}
}
}
Summary
In this post, we cover Spring HATEOAS and how to build hypermedia-driven Spring REST web service. Building a hypermedia feature for your REST API is really powerful and provides flexibility to the REST client to navigate and build the features without creating or thinking out the URL’s. This approach also provides flexibility to the server to change the URL structure or introducing new features without breaking the client API. It also let both clients as well as server API to evolve independently.
Source code for this post is available on the Github.