HATEOAS REST Services With Spring
In this article, we'll look at HATEAOS in a simple API that deals with customers, orders, and products.
Join the DZone community and get the full member experience.
Join For FreeWhat Is HATEOAS?
Hypermedia as the Engine of Application State, or HATEOAS for short, is a flavor of REST that uses hypermedia to describe what future actions are available to the client. Allowable actions are derived in the API based on the current application state and returned to the client as a collection of links. The client then uses these links to drive further interactions with the API.
Do I Need HATEOAS to Do REST?
In my opinion, no. Now that's a potentially controversial point of view, as many will argue that that only hypermedia enabled services are truly RESTful. I base my opinion on experience in the real world and the fact that I've built numerous REST APIs over the years that didn't use HATEOAS but served their purpose well. HATEOAS certainly has its place but it shouldn't be considered mandatory for RESTful APIs.
Varying Degrees of RESTfulness
At this point, I think it's worth taking a high-level look at REST to see where HATEOAS fits in. Richardson's Maturity Model provides a good overview by breaking the REST architectural style into various levels of maturity. These levels define how RESTful a system is, starting with level zero and working up to level 3.
- Level zero describes a system that uses HTTP as a transport mechanism only, also known as URI tunneling. A single URI and HTTP verb is typically used for all interactions with plain old XML being posted over the wire. Old school SOAP-RPC is a good example of level zero.
- Level one describes a system that builds on level zero by introducing the notion of resources. Resources typically represent a 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 behavior for each resource.
- Level three builds on level two by introducing HATEOAS. Hypermedia links are used to give the client a list of possible future actions. The list of actions is derived in the API based on the current application state.
The diagram below summarises Richardson's Maturity Model.
In this post, we'll look at level 3, hypermedia services. We'll look at HATEAOS in a simple API that deals with customers, orders, and products. We'll begin by looking at some sample HATEAOS responses and then move on to implement a simple hypermedia API
What Does HATEOAS Look Like?
So what exactly does HATEOAS look like? Let's look at a sample response from a GET call to the Customer API. As you'd expect, the response contains a JSON representation of the Customer, but it also contains a collection of hypermedia links.
{
"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"
}]
}
Each link has two attributes.
- rel — describes the relationship between the Customer resource and the URL (href attribute). Rel essentially describes the action that's performed with the link. It's important that this value is intuitive as it describes the purpose of the link.
- href — the URL used to perform the action described in rel.
In the example above, the first link has the rel value self and indicates that href is a link to the current Customer resource. In other words, if the client wants to retrieve a fresh copy of this Customer it can use the link associated with self. The next two links have rel values update and delete respectively. These are pretty intuitive and as you'd expect, provide links to endpoints for updating and deleting the current Customer resource. The final link has rel value orders and describes a link that retrieves all Order resources associated with this Customer.
Using Hypermedia to Navigate the API
After the client has retrieved a Customer, they can use the hypermedia in the response to drive further interactions with the API. The links provide a list of possible actions available to the client. The links in the sample JSON above allow the client to update, delete or retrieve a fresh copy of the Cutomer. The client can also retrieve a list of Orders associated with the Customer.
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. Finally, the client uses the Products link to retrieve all Products associated with an Order. The client is able to navigate their way through the API from a Customer to their Orders and then on to the Products associated with that Order.
Hold On, I Don't Have All the Information I Need!
At this point, you may have noticed that the links provided don't contain all the information needed to navigate the API. Take a look at the self, update and delete links in the Customer response. The URLs are the same but it doesn't tell us that self and delete should use request methods GET and DELETE respectively.
Ok, so you could make the argument that self and delete are straight forward because they map intuitively to HTTP verbs, but it's a bad idea to assume that clients will understand how links should be used. Imagine we have a link called deleteCustomerOrder. From a clients perspective, what exactly does this operation do? Does it remove the relationship between a Customer and an Order or does it delete the Order resource altogether? The answer is we simply don't know, and there is no way of providing this kind of information via hypermedia. That's where API documentation comes into play.
HATEOAS tells the client what options are available at a given point in time. It doesn't tell them how each link should be used or exactly what information should be sent. It's important to understand that although HATEOAS helps clients explore your API, it isn't a substitute for API documentation. Documentation is required to explain the semantics of each link (rel attribute) and how the associated URL should be used. Information such as content type, data model and request type still need to be described in the API documentation.
Building a Simple Hypermedia Service
We've looked at some HATEAOS responses and discussed a simple journey through the Customer API. At this point, you should have a fair idea of how the API should behave. Now its time to roll up our sleeves and start building the Customer API with Spring Boot. You can code along with this article, or if you're feeling lazy you can pull the code from Github before we start.
Introducing Spring HATEOAS
Adding hypermedia links to RESTful responses is something you could implement on your own, but if you're already using Spring for your REST layer, it's worth looking at Spring HATEOAS. Spring HATEOAS makes it easy to add hypermedia links to responses as you'll see in the following sections.
Domain Model — Adding ResourceSupport
As we saw in the sample response earlier, each entity has a collection of associated links. These links can be added to an entity via the ResoureSupport
class. The Customer
class below extends ResourceSupport
.
@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 Customer
. Spring HATEOAS provides a fluent API for adding links and uses the controller and method name of the endpoint you want to link to. This is a neat approach as Spring can then parse the target endpoint method to determine the structure of the link.
Start by importing both linkTo
and methodOn
from ControllerLinkBuilder
.
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
The getCustomer
endpoint below gets a Customer
from the database, adds 4 links to the Customer
and returns it to the client. We're able to add the links directly to Customer
because it extends ResourceSupport
.
@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 current endpoint, the getCustomer
method on the Customer
controller. getCustomer
takes a customerId 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 customerId 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 some arbitrary value.
Adding the above links to Customer
results in the following being returned to the client.
"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.
Order Controller — Get Customer Orders
The orders link shown above retrieves all CustomerOrder
resources associated with the specified Customer
. The endpoint for this link is shown below.
@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 CustomerOrder
entities are retrieved from the database. For each CustomerOrder
, a self-referencing link, a delete link and a products link are added. The approach is identical to that used in the Customer
controller, except this time the links refer to CustomerOrder
and a list of associated Product
entities. A sample response 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 Product
resources associated with a CustomerOrder
. This link is the final step that allows the client to navigate the API from Customer
to related CustomerOrder
, and finally through to Product
. The getProductsFromOrder
endpoint, as the name suggests, retrieves all Product
resources associated with a CustomerOrder
.
@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 Product
entities are retrieved from the database. For each Product
, a self-referencing link and a delete-from-order link is added.
At this point, you should be able to retrieve a Customer, then using the hypermedia links, navigate to associated Orders and then onto associated Products. If you had problems getting the app up and running don't forget you can grab the full source from Github.
Should I Use HATEOAS?
When it comes to architectural choices there are always tradeoffs. Before you consider using HATEOAS in the wild, you need to consider the pros and cons and whether or not you actually need it. Only then can you make an informed decision as to whether the extra complexity and effort are justified in your project.
Some Positives
Ready Made URLs
When URLs are hardcoded in the client, a change to the URL structure of the API introduces a breaking change. One of the benefits of HATEOAS is that the URL structure of the API can be changed without affecting clients. If the URL structure is changed in the service, clients will automatically pick up the new URL structure via hypermedia.
Being able to update an API (even if it's just the URL structure) without introducing breaking changes is a nice benefit. It's especially important for APIs with lots of clients, where you want to avoid the disruption and cost of breaking changes. If on the other hand, your REST API is used internally by one or two of your own apps, then such a breaking change can be dealt with quickly and probably isn't a big deal.
Explorable API
Hypermedia APIs are explorable. The links provide the client with a list of operations that can be called based on the current state of the application. This is useful for client developers as it can help them build a better mental model of the API and how it should be used.
As mentioned earlier, API documentation is still required to describe the semantics of each link along with important information like request message structure, request type and content type.
Workflow Style APIs
HATEOAS lends itself particularly well to APIs that process multiple steps as part of a user journey or workflow. Hypermedia is a powerful way of guiding clients toward the next step in the workflow by providing only the links that are relevant based on the current application state.
Some Negatives
Extra Complexity
HATEOAS adds complexity to the API, which affects both the API developer and those who consume it. The API developer needs to handle the extra work of adding links to each response and providing the correct links based on the current application state. This results in an application that's more complex to build and test than a vanilla CRUD REST API.
API clients also have to deal with the extra complexity of hypermedia. As well as having to understand the semantics of each link, they have extra data to parse and handle in each response. Although the benefits probably outweigh the cost of clientside complexity, it's worth bearing in mind that your decision to use HATEOAS will impose a certain amount of complexity on API clients.
Wrapping Up
In this post, we looked at HATEAOS and where it fits into the broader REST landscape. We looked at some sample hypermedia responses and saw how the links can be used to navigate through an API. We then built a simple hypermedia service using Spring Boot and Spring HATEOAS. Finally, we looked at some of the pros and cons of HATEOAS.
HATEOAS is a hotly debated topic, so I'm keen to hear your thoughts in the comments.
Opinions expressed by DZone contributors are their own.
Comments