Introduction
REST APIs are flexible and allow developers to make decoupled systems. With the rise of the microservice architecture - REST has matured even more as microservices can be built irrespective of the language or the framework used in the application.
Being "in the spotlight" - this means that new types get derived or built around REST APIs, which brings us to HATEOAS.
What is HATEOAS?
Being in the spotlight, different architecture techniques focused around REST fundamentals are being introduced.
Hypermedia as the Engine of Application State (HATEOAS) is an architectural approach to enhance the usability of REST APIs for the applications consuming the APIs.
The main purpose of HATEOAS is to provide extra information in REST API responses so that the API users can get additional endpoint details from a single call. This allows users to build their systems with dynamic API calls, moving from one endpoint to another using the information retrieved from each call.
To understand this better, take a look at the following API response:
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
}
Apart from getting the details about the doctor, the API response also provides extra information in the form of links. For example, a link to fetch all patients of a single doctor is attached too.
What we've got here is a resource-enriched response, where the provided links are resources that enrich our response with additional information.
Spring HATEOAS
Spring HATEOAS provides libraries to implement the HATEOAS architecture in a Spring application with ease. Using the Spring HATEOAS API, links can be created and returned as part of API response object.
Spring HATEOAS dependencies
Using Maven, adding Spring HATEOAS is as easy as including the dependencies:
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>[2.0.0.RELEASE,)</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>[1.0.3.RELEASE,)</version>
</dependency>
Alternatively, using Gradle, you can add:
implementation 'org.springframework.plugin:spring-plugin-core:2.+'
implementation 'org.springframework.hateoas:spring-hateoas:1.+'
Spring Boot HATEOAS dependencies
Even easier, for Spring Boot applications, you can use the spring-boot-starter-hateoas
Maven dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>[2.2.4.RELEASE,)</version>
</dependency>
Similarly, if you're using Gradle, you can simply add:
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.+'
Using the spring-boot-starter-hateoas
dependency includes spring-hateoas
and spring-boot-starter-web
dependencies, so naturally, no other starters are needed.
Spring HATEOAS Building Blocks
The basic building blocks for Spring HATEOAS are Link
s and RepresentationModel
s (a container for a collection of Link
s).
The RepresentationModel
is then extended into EntityModel
(for single resources) and CollectionModel
(for multiple resources), as well as a PagedModel
.
Let's take a brief moment to explain each of these before implementing them in a working demonstration.
Links
The immutable Link
object is used to store metadata for a resource (URI or location) - and the end-user can navigate to the resources that enrich our API response. A basic link with a resource URI could look like:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
The link contains a href
attribute, pointing to the URI of the resource. The href
attribute is wrapped inside a self
tag - which identifies the relationship with the entity. This means that the resource is pointing to itself, essentially.
Why have the resource point to itself?
The returned resources might not be the full representation of itself. A doctor can have a list of patients, but we might not want to return it by default.
If we'd then want to take a look at the doctor's list, we can navigate to it through the link.
Representation Models
The RepresentationModel
acts as a root class for all other Spring HATEOAS model classes. It contains a collection of Link
s and provides method to add/remove them.
Creating your own model is as easy as extending the RepresentationModel
class. Otherwise, you can use any of the readily available models:
-
Entity Model: The
EntityModel
is used to represent a resource that corresponds to a single object. You can wrap your resource with theEntityModel
and pass it to a calling service or return it through a REST endpoint. -
Collection Model: Similar to
EntityModel
, theCollectionModel
is used to wrap resources - though, it wraps a resource that corresponds to a collection of objects. -
Paged Model: Additionally, as many REST API endpoints return responses, which are pageable collections, Spring HATEOAS provides the
PagedModel
to represent such resources.
Creating Links
Let's create a sample resource which extends
the RepresentationModel
class:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private List<Patient> patientList;
}
For now our Doctor
model only has an id
property and a list of patients. Next, we'll be adding a Link
to the resource, which will point the resource to itself.
Link Object
Spring HATEOAS Link
objects take String
arguments to specify the URI and the relationship between the entities. These are basically the href
and rel
attributes:
Link selfLink = new Link("http://localhost:8080/doctors/1", "self");
Doctor doctor = new Doctor();
doctor.add(selfLink);
When a doctor object is returned (as shown in the demo application in later sections), the response body will contain:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
MVC LinkBuilder
Hardcoding values into the constructor of the Link
class isn't advised though. It quickly gets difficult to manage and update them as your application/API grows. To combat this, we can use the WebMvcLinkBuilder
, which allows us to create links using controller classes and pointing to their methods.
Let's recreate the link from the previous example using WebMvcLinkBuilder
:
Link link = linkTo(methodOn(DoctorController.class).getDoctorById(id)).withSelfRel();
Here, we're using the more programmatic approach to creating links. It points to the getDoctorById()
method inside the DoctorController
class. Since it points to itself, we use the withSelfRel()
method to specify the relationship.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Alternatively, we could've used the withRel()
method and passed a String with a different relationship.
Spring HATEOAS will translate the endpoint details from the controller class and the method that we have provided to the WebMvcLinkBuilder
. The output from this Link
object will be exactly the same as the one generated in the preceding example.
Relational Links
To create links for resources which have a relationship between them or they point to a different resource, we'd use the withRel()
method. Using this we can specify the endpoint with which the linked resource can be accessed:
Link link = linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId()))
.withRel("patientList");
The above code snippet specifies that the user can get the patientList
for the doctor
object, using the getDoctorPatients()
method inside DoctorController
class. When added to the response body, it generates the following link:
"_links": {
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
Notice that we didn't provide any URL when creating the link. Spring HATEOAS is able to extract the information from the link builder and generate a URL based on the mappings we've used.
Configuration
To properly render different RepresentationModel
subtypes, you can enable hyper media representation using the @EnableHypermediaSupport
annotation. You can pass the HypermediaType
as an argument to this annotation, allowing you to specify the hypermedia type, like JSON, UBER, HAL, etc. Using the annotation allows Spring to configure necessary Jackson modules to render hypermedia correctly.
Typically, Spring will detect the technology stack you're using and automatically adjust the configuration when you add the annotation. Though, if you have some custom requirements, we suggest you go through the official documentation.
Demo Application
With all that said, let's write a simple Spring application with HATEOAS support by going over to Spring Initializr and generating a blank Spring Boot application with the Spring HATEOAS
(spring-boot-hateoas-starter
) dependency:
Creating a Resource
For any resource which is to be exposed through the REST API, it needs to extend RepresentationModel
. By extending the RepresentationModel
class, we also inherit the add()
method, which is used to attach links to it.
Lets create a model for a Doctor
:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private String name;
private String speciality;
private List<Patient> patientList;
}
As the Doctor
class has a relationship with patients, let's create the Patient
model as well:
public class Patient extends RepresentationModel<Patient> {
private int id;
private String name;
}
Next, in a controller, in our case a DoctorController
, we'll autowire the DoctorService
:
@RestController
@RequestMapping(value = "/doctors")
public class DoctorController {
@Autowired
DoctorService doctorService;
}
As you might expect, it contains methods such as getDoctor()
, getDoctorWithPatients()
, getDoctors()
, etc, which all return a Doctor
or a List<Doctor>
. The implementation is omitted for brevity - if you'd like to take a look, the code is up on GitHub.
With this, we've created a resource. When retrieving resources, we'll either expect a single resource or a collection of resources. As stated earlier, we'll wrap them in an EntityModel
or CollectionModel
, respectively.
Retrieving a Single Resource
Let's first implement the functionality of fetching a single doctor. Since we expect the API call to return a single resource, we will wrap our response inside an EntityModel
class:
@GetMapping(value = "/{id}")
public EntityModel<Doctor> getDoctorById(@PathVariable int id) {
Doctor doctor = doctorService.getDoctorWithPatients(id);
for (final Patient patient : doctor.getPatientList()) {
Link selfLink = linkTo(methodOn(PatientController.class)
.getPatientById(patient.getId())).withSelfRel();
patient.add(selfLink);
}
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorById(id)).withSelfRel());
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId())).withRel("patientList"));
return new EntityModel<>(doctor);
}
After retrieving the Doctor
object, we are traversing over the list of associated patients and adding a link for each of them. Each of these links can be used to get each individual Patient
through the PatientController
.
Similarly, we are adding a self
link to the Doctor
which was used to make the API calls. Along with the self link we are also adding a relational link, pointing to the patient list.
At the end of the method we have wrapped our Doctor
object in an EntityModel
class and this EntityModel
is returned as the response:
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
},
{
"id": 2,
"name": "Samantha Williams",
"_links": {
"self": {
"href": "http://localhost:8080/patients/2"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
}
"Dr. Sanders" has "J. Smalling" and "Samantha Williams" as their patients and both the endpoint for the doctor and the endpoint for a list of the doctor's patients are added to the response - making it resource-enriched.
Retrieving Multiple Resources
Let's create another GET call which returns all the available doctors in the system. Now that the response we are expecting will be a collection of Doctor
objects, we will wrap the response inside the CollectionModel
:
@GetMapping
public CollectionModel<Doctor> getDoctors() {
List<Doctor> doctors = doctorService.getDoctorsWithPatients();
for (final Doctor doctor : doctors) {
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorById(doctor.getId())).withSelfRel());
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId())).withRel("patientList"));
for (final Patient patient : doctor.getPatientList()) {
Link selfLink = linkTo(methodOn(PatientController.class)
.getPatientById(patient.getId())).withSelfRel();
patient.add(selfLink);
}
}
Link link = linkTo(methodOn(DoctorController.class).getDoctors()).withSelfRel();
return new CollectionModel<>(doctors, link);
}
In this method, along with the self
link for the REST call itself, we are also adding a self link to retrieve each individual doctor. Each doctor has a relational link, which points to the associated patients. Inside the patient list, each patient also has a self
link, which can be used to retrieve the specific patient as well.
Once all the links are added we have wrapped the collection of Doctor
objects inside a CollectionModel
and returned it:
{
"_embedded": {
"doctorList": [
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
},
{
"id": 2,
"name": "Dr. Goldberg",
"speciality": "General",
"patientList": [
{
"id": 4,
"name": "K. Oliver",
"_links": {
"self": {
"href": "http://localhost:8080/patients/4"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/2"
},
"patientList": {
"href": "http://localhost:8080/doctors/2/patients"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/doctors"
}
}
}
As you can see from the output, just by making a single call, the user can discover additional information, which wouldn't otherwise be present.
Conclusion
Spring HATEOAS provides the required libraries and infrastructure to implement the HATEOAS architecture in Spring-based applications.
As evident from the outputs, users can discover additional information from a single REST call. Using this information, it's easier to build dynamic REST clients.
In this article, we've discussed how HATEOAS works, Spring's implementation of it, and finished by building a simple application to demonstrate the concepts.
The source code for the sample code can be found here on GitHub.