REST HATEOAS with Spring
The Richardson maturity model breaks the REST architectural style into various levels of maturity.
- Level zero describes a system that uses HTTP as a transport mechanism only (also known as URI tunnelling). A single URI and HTTP verb is typically used for all interactions with POX (plain old XML) being posted over the wire. Old school SOAP-RPC is a good level zero example.
- Level one describes a system that builds on level zero by introducing the notion of resources. Resources typically represent some business entity and are usually described using nouns. Each resource is addressed via a unique URI and a single HTTP verb is used for all interactions.
- Level two builds on level one by using a broader range of HTTP verbs to interact with each resource. Typically GET, POST, PUT and DELETE are used to retrieve, create, update and delete resources, providing consumers with CRUD behaviour for each resource.
- Level three builds upon level two by introducing HATEOAS – Hypermedia As The Engine Of Application State. HATEOAS describes a RESTful system that returns hypermedia links with each response, providing the consumer with options for subsequent API calls. Hypermedia links help guide consumers by providing options for further calls based on the current application state.
Its probably fair to say that most RESTful applications achieve level two in Richardsons Maturity Model (certainly the case for most of the RESTful applications I’ve worked on). This post will look at level three and how you can implement HATEOAS in a Spring based application. Side note – if you’re looking for a good example of a level 2 REST service take a look at this post.
Sample HATEOAS response
Before we write any code lets look at a sample HATEOAS response from the Customer endpoint we’re going to write. The following is returned as the result of a HTTP GET request and includes a JSON representation of the Customer, as well a collection of hypermedia links. Each link contains a rel and href attribute. The rel attribute describes the relationship between the Customer and the supplied link, while the href attribute contains the actual URL.
{ "customerId": 1, "firstName": "Joe", "lastName": "Smith", "dateOfBirth": "1982-01-10", "address": { "id": 1, "street": "High Street", "town": "Newry", "county": "Down", "postcode": "BT893PY" }, "links": [{ "rel": "self", "href": "http://localhost:8080/api/customer/1" }, { "rel": "update", "href": "http://localhost:8080/api/customer/1" }, { "rel": "delete", "href": "http://localhost:8080/api/customer/1" }, { "rel": "orders", "href": "http://localhost:8080/api/customer/1/orders" }] }
The first link uses the rel value self to indicate that this is a self referencing link. In other words this link can be used to retrieve this Customer definition. The next two links use the rel values update and delete respectively. These are pretty intuitive and as you’d expect provide links to endpoints for updating and deleting a Customer resource. The final link uses the rel value orders and provides a link for retrieving all Customer Orders associated with this Customer.
Navigating the API
Using the links provided, the client can update or delete the current Customer resource, or retrieve a list of Orders associated with the Customer. The orders link is particularly interesting as it allows the client to navigate the API to retrieve Order data related to this Customer. Following hypermedia links allows a client to navigate an API to find the data that they want.
The diagram below describes a simple journey through the API. The client begins by retrieving a Customer resource, then uses the order link to retrieve a list of associated Orders, and finally uses the products links to retrieve all products associated with each Order.
HATEOAS – No Substitute For API Documentation
You may have noticed that the links provided do not include all the information required to make the call, like the HTTP verb to use for example. Take a look at the update and delete links in the Customer response. Both URL values are the same but it isn’t specified that update should be called with a HTTP PUT and delete should be called with a HTTP DELETE. Update and delete are trivial examples because they map intuitively to HTTP verbs, but what if we had a link like deleteCustomerOrder. Would such a link simply remove the relationship between the Customer and the Order or would it delete the Order resource altogether? The answer is we simply don’t know, without consulting the API documentation.
HATEOAS tells the consumer what options are available for a given resource. It doesn’t tell them the circumstances in which each link should be used or what information should be sent. Its important to understand that HATEOAS is not a substitute for formal API documentation. Documentation should be provided that explains the semantics of each link (via the rel attribute) and how that associated link should be used. Information such as expected data structure, content type and HTTP verb should be expressed through documentation.
Implementing HATEOAS
Now that we’ve covered some of the theory and looked at a few examples, lets take a look at a simple implementation. A sample application with integration tests accompanies this post, so feel free to pull the code from Github. It might help to have the full source code locally before going any further.
Introducing Spring HATEOAS
Spring HATEOAS makes it easy to add hypermedia links by providing some useful utilities via the ControllerLinkBuilder class. The sample code in this post makes use of the linkTo and methodOn methods. These can be statically imported as follows.
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
Domain Model – Adding ResourceSupport
As shown in the sample HATEOAS responses, each exposed entity can have a list of associated links. We can use Spring HATEOAS to make these links available on each entity via the ResourceSuppport base class as follows.
@Entity @ToString public class Customer extends ResourceSupport { public Customer() { } public Customer(String firstName, String lastName, LocalDate dateOfBirth, Address address) { this.firstName = firstName; this.lastName = lastName; this.dateOfBirth = dateOfBirth; this.address = address; } @Id @Getter @GeneratedValue(strategy=GenerationType.AUTO) private long customerId; @Setter @Getter private String firstName; @Setter @Getter private String lastName; @Setter @Getter private LocalDate dateOfBirth; @Setter @Getter @OneToOne(cascade = {CascadeType.ALL}) private Address address; @Setter @Getter @JsonBackReference @OneToMany(cascade = { CascadeType.ALL }) private Set<CustomerOrder> orders; public void addOrder(CustomerOrder order){ if(orders == null){ orders = new HashSet<>(); } orders.add(order); } }
Customer Controller – Get Customer
With ResourceSupport added we can now add links directly to the Customer entity. Spring HATEOAS provides a fluent API for adding links, using the controller and method name of the endpoint you want to link to. This is a neat approach as Spring can easily parse the endpoint method to determine the structure of the link. It’s up to the developer to provide values for any arguments that are required to the endpoint.
@RequestMapping(value = "/api/customer/{customerId}", method = RequestMethod.GET) public ResponseEntity<Customer> getCustomer(@PathVariable("customerId") Long customerId) { /* validate Customer Id parameter */ if (null==customerId) { throw new InvalidCustomerRequestException(); } Customer customer = customerRepository.findOne(customerId); if(null==customer){ throw new CustomerNotFoundException(); } customer.add(linkTo(methodOn(CustomerController.class) .getCustomer(customer.getCustomerId())) .withSelfRel()); customer.add(linkTo(methodOn(CustomerController.class) .updateCustomer(customer, customer.getCustomerId())) .withRel("update")); customer.add(linkTo(methodOn(CustomerController.class) .removeCustomer(customer.getCustomerId())) .withRel("delete")); customer.add(linkTo(methodOn(OrderController.class) .getCustomerOrders(customer.getCustomerId())) .withRel("orders")); return ResponseEntity.ok(customer); }
Lines 15 to 17 create a link to the getCustomer method on the Customer controller, which in this case is actually the current method. The getCustomer method takes a single Customer Id parameter, so we have to supply this value when constructing the link. We’re building a self referencing link in this instance so we use the Customer Id of the current Customer. After specifying the target controller and method, we need to supply a rel value. We have two options here, either withSelfRel() to specify self, or withRel(“anyValue”) to specify an arbitrary value.
The links created for self, update, delete and orders are as follows.
"links": [{ "rel": "self", "href": "http://localhost:8080/api/customer/1" }, { "rel": "update", "href": "http://localhost:8080/api/customer/1" }, { "rel": "delete", "href": "http://localhost:8080/api/customer/1" }, { "rel": "orders", "href": "http://localhost:8080/api/customer/1/orders" }]
The update, delete and orders links are created in exactly the same way. Note that orders is a link created between resources, allowing the client to view the relationship between Customers and Orders in this instance.
Order Controller – Get Customer Orders
The orders link shown above allows the client to retrieve all Orders associated with the specified Customer. Like the Customer entity, the CustomerOrder entity also extends ResourceSupport so that links can be added. The getCustomerOrders endpoint is defined as follows.
@RequestMapping(value = "/api/customer/{customerId}/orders", method = RequestMethod.GET) public ResponseEntity<Set<CustomerOrder>> getCustomerOrders(@PathVariable("customerId") Long customerId) { Set<CustomerOrder> orders = customerRepository.findOne(customerId).getOrders(); orders.forEach(order -> { order.add(linkTo(methodOn(OrderController.class) .getOrder(order.getOrderId())) .withSelfRel()); order.add(linkTo(methodOn(OrderController.class) .removeorder(order.getOrderId())) .withRel("delete")); order.add(linkTo(methodOn(OrderController.class) .getProductsFromOrder(order.getOrderId())) .withRel("products")); }); return ResponseEntity.ok(orders); }
A Set of Orders is retrieved from the database using the supplied Customer Id. For each Order returned, a self referencing link, a delete link and a products link is added. The approach is identical to that used in the Customer controller, except this time the links refer to the CustomerOrder entity and a list of Products associated with the CustomerOrder. A sample JSON response from this endpoint is shown below.
[{ "orderId": 1, "orderDate": "2017-07-13", "dispatchDate": "2017-07-16", "totalOrderAmount": 783.99, "links": [{ "rel": "self", "href": "http://localhost:8080/api/order/1" }, { "rel": "delete", "href": "http://localhost:8080/api/order/1" }, { "rel": "products", "href": "http://localhost:8080/api/order/1/products" }] }, { "orderId": 3, "orderDate": "2017-07-13", "dispatchDate": "2017-07-14", "totalOrderAmount": 69.98, "links": [{ "rel": "self", "href": "http://localhost:8080/api/order/3" }, { "rel": "delete", "href": "http://localhost:8080/api/order/3" }, { "rel": "products", "href": "http://localhost:8080/api/order/3/products" }] }, { "orderId": 2, "orderDate": "2017-07-13", "dispatchDate": "2017-07-15", "totalOrderAmount": 619.99, "links": [{ "rel": "self", "href": "http://localhost:8080/api/order/2" }, { "rel": "delete", "href": "http://localhost:8080/api/order/2" }, { "rel": "products", "href": "http://localhost:8080/api/order/2/products" }] }]
Product Controller – Get Products From Order
The products link shown above allows the client to retrieve a list of Products associated with an Order. This link provides the final step in allowing the client to navigate the API, from Customer to related Orders, and finally through to Products. The getProductsFromOrder endpoint as the names suggests, retrieves all Products associated with an Order and is defined as follows.
@RequestMapping(value = "/api/order/{orderId}/products", method = RequestMethod.GET) public ResponseEntity<Set<Product>> getProductsFromOrder(@PathVariable("orderId") Long orderId) { Set<Product> products = orderRepository.findOne(orderId).getProducts(); products.forEach(product -> { product.add(linkTo(methodOn(ProductController.class) .getProduct(product.getProductId())) .withSelfRel()); product.add(linkTo(methodOn(OrderController.class) .deleteProductFromOrder(orderId, product.getProductId())) .withRel("delete-from-order")); }); return ResponseEntity.ok(products); }
A Set of Products are retrieved from the database using the supplied Order Id. For each Product returned a self referencing link and a delete-from-order link is added.
Why use HATEOAS?
Ready Made URLs
I like the idea of providing the consumer with ready made URLs. The consumer needs to understand the semantic context of each link before they can use it, but complexity is reduced as URLs no longer have to built for each request. The real benefit comes from the fact that the API can be changed, including the URL structure, without affecting consumers. Client applications simply use URLs supplied via hypermedia links and when the structure of those links change, the client remains oblivious. Being able to update an API without introducing breaking changes is a huge benefit, especially for APIs that are public facing or have a large number if consumers.
Self Documenting APIs (sort of…)
HATEOAS systems are to a certain degree self documenting. The links provide the consumer with a list of operations that can performed based on the current state of the application. This is can be a useful starting point during development but is not a substitute for formal documentation.
API documentation is still required to describe the semantics of each link as well as the information required to use it. Request message structure, HTTP verb and content type are not documented by HATEOAS, yet this detail is essential for developers calling the service.
Wrapping Up
[/fusion_text][/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]
Leave A Comment