Course – LS (cat=REST) (INACTIVE)

Get started with Spring and Spring Boot, through the reference Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

This article explains the process of creating hypermedia-driven REST web service using the Spring HATEOAS project.

2. Spring-HATEOAS

The Spring HATEOAS project is a library of APIs that we can use to easily create REST representations that follow the principle of HATEOAS (Hypertext as the Engine of Application State).

Generally speaking, the principle implies that the API should guide the client through the application by returning relevant information about the next potential steps, along with each response.

In this article, we’re going to build an example using Spring HATEOAS with the goal of decoupling the client and server, and theoretically allowing the API to change its URI scheme without breaking clients.

3. Preparation

First, let’s add the Spring HATEOAS dependency:

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

If we’re not using Spring Boot we can add the following libraries to our project:

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
    <version>1.4.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.plugin</groupId>
    <artifactId>spring-plugin-core</artifactId>
    <version>1.2.0.RELEASE</version>
</dependency>

As always, we can search the latest versions of the starter HATEOAS, the spring-hateoas and the spring-plugin-core dependencies in Maven Central.

Next, we have the Customer resource without Spring HATEOAS support:

public class Customer {

    private String customerId;
    private String customerName;
    private String companyName;

    // standard getters and setters
}

And we have a controller class without Spring HATEOAS support:

@RestController
@RequestMapping(value = "/customers")
public class CustomerController {
    @Autowired
    private CustomerService customerService;

    @GetMapping("/{customerId}")
    public Customer getCustomerById(@PathVariable String customerId) {
        return customerService.getCustomerDetail(customerId);
    }
}

Finally, the Customer resource representation:

{
    "customerId": "10A",
    "customerName": "Jane",
    "customerCompany": "ABC Company"
}

4. Adding HATEOAS Support

In a Spring HATEOAS project, we don’t need to either look up the Servlet context nor concatenate the path variable to the base URI.

Instead, Spring HATEOAS offers three abstractions for creating the URI – RepresentationModel, Link, and WebMvcLinkBuilder. We can use these to create the metadata and associate it to the resource representation.

4.1. Adding Hypermedia Support to a Resource

The project provides a base class called RepresentationModel to inherit from when creating a resource representation:

public class Customer extends RepresentationModel<Customer> {
    private String customerId;
    private String customerName;
    private String companyName;
 
    // standard getters and setters
}

The Customer resource extends from the RepresentationModel class to inherit the add() method. So once we create a link, we can easily set that value to the resource representation without adding any new fields to it.

Spring HATEOAS provides a Link object to store the metadata (location or URI of the resource).

First, we’ll create a simple link manually:

Link link = new Link("http://localhost:8080/spring-security-rest/api/customers/10A");

The Link object follows the Atom link syntax and consists of a rel which identifies relation to the resource and href attribute which is the actual link itself.

Here’s how the Customer resource looks now that it contains the new link:

{
    "customerId": "10A",
    "customerName": "Jane",
    "customerCompany": "ABC Company",
    "_links":{
        "self":{
            "href":"http://localhost:8080/spring-security-rest/api/customers/10A"
         }
    }
}

The URI associated with the response is qualified as a self link. The semantics of the self relation is clear – it’s simply the canonical location the resource can be accessed at.

Another very important abstraction offered by the library is the WebMvcLinkBuilder – which simplifies building URIs by avoiding hard-coded the links.

The following snippet shows building the customer self-link using the WebMvcLinkBuilder class:

linkTo(CustomerController.class).slash(customer.getCustomerId()).withSelfRel();

Let’s have a look:

  • the linkTo() method inspects the controller class and obtains its root mapping
  • the slash() method adds the customerId value as the path variable of the link
  • finally, the withSelfMethod() qualifies the relation as a self-link

5. Relations

In the previous section, we’ve shown a self-referencing relation. However, more complex systems may involve other relations as well.

For example, a customer can have a relationship with orders. Let’s model the Order class as a resource as well:

public class Order extends RepresentationModel<Order> {
    private String orderId;
    private double price;
    private int quantity;

    // standard getters and setters
}

At this point, we can extend the CustomerController with a method that returns all orders of a particular customer:

@GetMapping(value = "/{customerId}/orders", produces = { "application/hal+json" })
public CollectionModel<Order> getOrdersForCustomer(@PathVariable final String customerId) {
    List<Order> orders = orderService.getAllOrdersForCustomer(customerId);
    for (final Order order : orders) {
        Link selfLink = linkTo(methodOn(CustomerController.class)
          .getOrderById(customerId, order.getOrderId())).withSelfRel();
        order.add(selfLink);
    }
 
    Link link = linkTo(methodOn(CustomerController.class)
      .getOrdersForCustomer(customerId)).withSelfRel();
    CollectionModel<Order> result = CollectionModel.of(orders, link);
    return result;
}

Our method returns a CollectionModel object to comply with the HAL return type, as well as a “_self” link for each of the orders and the full list.

An important thing to notice here is that the hyperlink for the customer orders depends on the mapping of getOrdersForCustomer() method. We’ll refer to these types of links as method links and show how the WebMvcLinkBuilder can assist in their creation.

The WebMvcLinkBuilder offers rich support for Spring MVC Controllers. The following example shows how to build HATEOAS hyperlinks based on the getOrdersForCustomer() method of the CustomerController class:

Link ordersLink = linkTo(methodOn(CustomerController.class)
  .getOrdersForCustomer(customerId)).withRel("allOrders");

The methodOn() obtains the method mapping by making dummy invocation of the target method on the proxy controller and sets the customerId as the path variable of the URI.

7. Spring HATEOAS in Action

Let’s put the self-link and method link creation all together in a getAllCustomers() method:

@GetMapping(produces = { "application/hal+json" })
public CollectionModel<Customer> getAllCustomers() {
    List<Customer> allCustomers = customerService.allCustomers();

    for (Customer customer : allCustomers) {
        String customerId = customer.getCustomerId();
        Link selfLink = linkTo(CustomerController.class).slash(customerId).withSelfRel();
        customer.add(selfLink);
        if (orderService.getAllOrdersForCustomer(customerId).size() > 0) {
            Link ordersLink = linkTo(methodOn(CustomerController.class)
              .getOrdersForCustomer(customerId)).withRel("allOrders");
            customer.add(ordersLink);
        }
    }

    Link link = linkTo(CustomerController.class).withSelfRel();
    CollectionModel<Customer> result = CollectionModel.of(allCustomers, link);
    return result;
}

Next, let’s invoke the getAllCustomers() method:

curl http://localhost:8080/spring-security-rest/api/customers

And examine the result:

{
  "_embedded": {
    "customerList": [{
        "customerId": "10A",
        "customerName": "Jane",
        "companyName": "ABC Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A"
          },
          "allOrders": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
          }
        }
      },{
        "customerId": "20B",
        "customerName": "Bob",
        "companyName": "XYZ Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/20B"
          },
          "allOrders": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/20B/orders"
          }
        }
      },{
        "customerId": "30C",
        "customerName": "Tim",
        "companyName": "CKV Company",
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/30C"
          }
        }
      }]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/spring-security-rest/api/customers"
    }
  }
}

Within each resource representation, there is a self link and the allOrders link to extract all orders of a customer. If a customer doesn’t have orders, then the link for orders won’t appear.

This example demonstrates how Spring HATEOAS fosters API discoverability in a rest web service. If the link exists, the client can follow it and get all orders for a customer:

curl http://localhost:8080/spring-security-rest/api/customers/10A/orders
{
  "_embedded": {
    "orderList": [{
        "orderId": "001A",
        "price": 150,
        "quantity": 25,
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/001A"
          }
        }
      },{
        "orderId": "002A",
        "price": 250,
        "quantity": 15,
        "_links": {
          "self": {
            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/002A"
          }
        }
      }]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
    }
  }
}

8. Conclusion

In this tutorial, we’ve discussed how to build a hypermedia-driven Spring REST web service using the Spring HATEOAS project.

In the example, we see that the client can have a single entry point to the application and further actions can be taken based on the metadata in the response representation.

This allows the server to change its URI scheme without breaking the client. Also, the application can advertise new capabilities by putting new links or URIs in the representation.

Finally, the full implementation of this article can be found in the GitHub project.

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
Course – LS (cat=REST)

Get started with Spring and Spring Boot, through the Learn Spring course :

>> CHECK OUT THE COURSE
res – REST (eBook) (cat=REST)
Comments are closed on this article!