Spring HATEOAS tutorial
In this article, we will explore what is HATEOAS, what problem does it solve and how to add support for HATEOAS in spring and spring boot applications with examples.
HATEOAS is an acronym for Hypermedia As the Engine of Application State.
The term hypermedia is used to describe a system that provides links to resources so that the client can discover the application state.
HATEOAS principle states that a client should be able to navigate an application without having to hard-code any URLs
Imagine a client which interacts with a REST service that fetches all books in a library.
The endpoint that the client accesses is
app/books/all
and below is the response
[ { "id":1, "title":"Together For A Day" "genre": "Drama" }, { "id":2, "title":"The Prince of Darkness" "genre": "Thriller" }, { "id":3, "title":"The New Days" "genre": "Drama" }, { "id":4, "title":"Stay Hidden" "genre": "Horror" } ]
Now, suppose the client needs to fetch details of a book using its id or any other identifier.
But, it does not know how to do that, that is, which endpoint it should access.
What if the application sends the link or endpoint to access the details of a book in the above response.
This is what HATEOAS principle is.
A client interacting with an application with HATEOAS implemented gets the navigation links in the responses.
HATEOAS Benefits
1. Since the client get the API endpoints in the response, there is no need to hard-code the URLs.
2. Decouples the client and server. You can make changes to the server-side of your API without having to update the client. This allows you to roll out new features and functionality without having to redeploy the client.
3. It makes your API infinitely more flexible and scalable.
HATEOAS in Spring
Spring provides the support of HATEOAS by adding following dependencies to the project
// MAVEN <dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>2.0.0</version> </dependency> // GRADLE implementation 'org.springframework.hateoas:spring-hateoas:2.0.0'
If you are using spring boot, then add below starter dependency to add HATEOAS support in the application.
// MAVEN <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> <version>3.0.0</version> </dependency> // GRADLE implementation 'org.springframework.boot:spring-boot-starter-hateoas:3.0.0'
Before understanding how to add HATEOAS support in Spring application, go through below classes.
Controller class
@RestController @RequestMapping("app") public class BookController { @GetMapping("books/all") public List<Book> getAllBooks() { // return list of books } @GetMapping("books/detail/{id}") public Book getBookById(@PathVariable Integer id) { // fetch book by its id } }
Below is the Book class
public class Book { private Integer id; private String title; private String genre; // getters and setters }
Now, to add HATEOAS support, you need to remember three classes
1. RepresentationModel
RepresentationModel
is a Spring class that allows to add links to domain classes with its addLink()
method.
So, if you want to add a link to resource, you need to extend your domain class(Book
, in our case) with this class.
2. Link
Each link is represented by an object of type Link, which is another Spring class.
It contains two properties href
, which is the URL of a resource, and rel
, which specifies the relation or an identifier of the link.
These properties are similar to the attributes of anchor(<a>
) tag in HTML.
3. WebMvcLinkBuilder
This class is used to build links to resources with its below static methods.
A. linkTo()
Specifies the controller class whose methods we need to link. The class is supplied as an argument.
B. methodOn()
Specifies the method of the controller class which will be linked.
C. slash()
Used to insert a slash as a separator between URL components. Example, books/detail/1
.
Inserting Links
Now, suppose we want to insert a link to all books in the response of a single book fetch request.
That is, when the client requests a single book, we should also return the URL of all books in the response.
For this, we need to modify Book
to extend RepresentationModel
as below
public class Book extends RepresentationModel<Book> { // fields }
This will provide addLink()
method to book object.
No other change is required in Book
.
Other change needs to be done in controller method which fetches the book by its id as shown below.
@GetMapping("books/detail/{id}") public Book getBookById(@PathVariable Integer id) { List<Book> books = getBooks(); Book book = books. stream(). filter(u -> u.getId().equals(id)). findFirst(). get(); // create link to all books method Link all = WebMvcLinkBuilder. linkTo(BookController.class). slash("books"). slash("all"). withRel("books"); // add link to response object book.add(all); return book; }
Link to all books according to our application is
http://localhost:8080/app/books/all
To create this link, we use this block
WebMvcLinkBuilder. linkTo(BookController.class). slash("books"). slash("all"). withRel("books");
Here,
WebMvcLinkBuilder.linkTo(BookController.class)
will create link till the root of controller class, that is, http://localhost:8080/app
To add books/all
after this, we use slash()
containing the first part, which is, “books” followed by another slash containing the second path, which is “all”.
The key with which this link will appear in the response is created using withRel()
method.
Now, when we access books/detail/1
, the response will be
{ "id":1, "title":"Together For A Day", "genre":"Drama", "_links": { "books": { "href":"http://localhost:8081/app/books/all" } } }
Notice that _links
attribute is automatically added.
It contains the link of all the books that we created.
Removing URL string
In the above example, we used string values(“books” and “all”) to create URL to the list of books.
This is not correct, since if the URL changes, then we need to change the link definition as well.
Secondly, using a string becomes error prone since you might change the string at one place and forget at another without raising a compiler error.
To solve these problems, WebMvcLinkBuilder
provides a methodOn()
method, which fetches all the methods of the controller class.
So, instead of using the URL mapping, we can use the method name which handles that request. Modified code is
@GetMapping("books/detail/{id}") public Book getBookById(@PathVariable Integer id) { // get list of all books List<Book> books = getBooks(); Book book = books.stream(). filter(u -> u.getId().equals(id)). findFirst().get(); // create link to all books method Link all = WebMvcLinkBuilder. linkTo(WebMvcLinkBuilder. methodOn(BookController.class, getAllBooks() ). withRel("books"); // add link to response object book.add(all); return book; }
This method will automatically collect the URL mapping of getAllBooks()
method and add it to link.
So, instead of manually creating URL, we reused it.
To shorten the code, you can import methodOn()
and linkTo()
statically as shown below
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
Now, the above code may also be written as
@GetMapping("books/detail/{id}") public Book getBookById(@PathVariable Integer id) { // get list of all books List<Book> books = getBooks(); Book book = books.stream(). filter(u -> u.getId().equals(id)). findFirst(). get(); // create link to all books method Link all = linkTo( methodOn(BookController.class, getAllBooks()). withRel("books"); // add link to response object book.add(all); return book; }
Self link refers the link to the same resource which is accessed. So, when trying to access a book by its id, self link will be the URL of the book which is accessed.
To add a self link, use
withSelf()
method instead of withRel()
method as shown below Link self = WebMvcLinkBuilder.linkTo(BookController.class). slash("detail"). slash(book.getId()). withSelfRel();
So, now when you access URL, app/books/detail/1
, the response will be
{ "id":1, "title":"Together For A Day", "genre":"Drama", "_links": { "books": { "href":"http://localhost:8081/app/books/all" }, "self": { "href":"http://localhost:8081/app/books/detail/1" } } }
Notice the “self” key contains the link to a single book.
Adding links to a collection
Till now, we had a single record in the response. What if the response is a list of books and we want to add link in this response.
For this purpose, we need to use CollectionModel
class from Spring HATEOAS as below
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @RestController @RequestMapping("/books") public class BookController { @GetMapping public CollectionModel<Book> getAllBooks() { List<Book> books = getBooks(); List<Book> resources = new ArrayList<>(); for (Book book : books) { resources.add(new Book(book)); } Link link = linkTo( methodOn(BookController.class).getAllBooks()). withSelfRel(); return CollectionModel.of(bookResources, link); } }
In this example, getAllBooks()
method of BookController
class returns a CollectionModel
object, which represents a collection of Book
objects.
It also creates a self-link to getAllBooks()
method of the BookController
class using the linkTo()
method, as explained earlier.
Response of this method is given below
{ "_embedded":{ "bookList":[ { "id":1, "title":"Together For A Day", "genre":"Drama", "_links":{ "self":{ "href":"http://localhost:8081/books/1" } } }, { "id":2, "title":"The Prince of Darkness", "genre":"Thriller", "_links":{ "self":{ "href":"http://localhost:8081/books/2" } } }, { "id":3, "title":"The New Days", "genre":"Drama", "_links":{ "self":{ "href":"http://localhost:8081/books/3" } } }, { "id":4, "title":"Stay Hidden", "genre":"Horror", "_links":{ "self":{ "href":"http://localhost:8081/books/4" } } } ] }, "_links":{ "self":{ "href":"http://localhost:8081/books" } } }
For adding a self link to each book, we also need to modify Book
class to add a constructor as below
public Book(Book book) { this.id = book.getId(); this.title = book.getTitle(); this.genre = book.getGenre(); add(linkTo(methodOn(BookController.class).getBookById(book.getId())).withSelfRel()); }
This constructor refers to getBookById()
method in BookController
class to create self link.
Conclusion
In this article, understood HATEOAS (Hypermedia as the Engine of Application State) principle and its implementation in Spring Boot using the Spring HATEOAS library.
Spring boot HATEOAS allows us to create RESTful APIs that provide a hypermedia-driven representation of resources, allowing clients to navigate the API and discover available resources dynamically.
By providing a standardized way of representing resources and their relationships, HATEOAS can make it easier to create and maintain RESTful APIs and improve the overall developer experience.