Unlocking Dynamic RESTful APIs with Spring Boot: A Deep Dive into HATEOAS

Hypermedia as the Engine of Application State is referred to as HATEOAS. As part of the REST (Representational State Transfer) architectural style, it allows the client to communicate with the RESTful service completely through dynamic hyperlinks that are supplied by the service. The client-side requires a basic knowledge of how to communicate with a server.
Interaction between client and the server.
The client send a message and wait till the server response. The server receive the message and the web service performance the action. Then the message sent back to client.
Example: Assume you are in France with a friend who speaks both English and French, but you do not speak French. You asked your pal to ask someone who knows the location since you need to find a place. After that, your pal asks someone where it is and tells you back.

Here you are the client and friend is the web server who understand the client. And web server communicate with the server to do the job.
Introduction to HATEOAS
Using a browser, users can interact with buttons, input fields, links, and other elements on a webpage, allowing for dynamic interactions. Conventional API answers, on the other hand, usually offer static data with no actions or instructions attached, making it more difficult for the client to navigate or carry out further tasks without interruption. Hypermedia as the Engine of Application State, or HATEOAS, enters the picture here. A HATEOAS request allows you to not only send the data but also specify the related actions.
Example :- Imagine you’re a traveler embarking on a journey through a vast and unfamiliar city. At the city’s entrance, you receive a detailed map that not only highlights your current location but also marks various points of interest, such as museums, restaurants, and parks. Each point of interest on the map includes information about what you can do there and how to get to the next destination.

As you explore, you refer to this map to decide your next steps. The map dynamically guides you from one location to another, suggesting possible activities and providing directions, without you needing prior knowledge about the city’s layout or attractions.
In this analogy:
- You represent the client application.
- The map serves as the server’s response.
- The points of interest and their connecting paths symbolize the hypermedia links provided in a HATEOAS-compliant API response.
Steps to Implement HATEOAS in Spring Boot
Let’s construct a example for an employee payroll system that allows us to retrieve an employee’s salary information and include hypermedia links for additional actions (such as reading the employee’s information or altering the salary).
1. Add the Spring HATEOAS Dependency
Add spring-boot-starter-hateoas dependency in the Spring boot project in pom.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
2. Create a Resource Representation
Create a domain model for Employee.
public class Employee {
private Long id;
private String name;
private String role;
private double salary;
// Getters and Setters
}
3. Create a Resource Representation
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
public class EmployeeResource extends EntityModel<Employee> {
public EmployeeResource(Employee employee) {
super(employee);
add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployee(employee.getId())).withSelfRel());
add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployeeSalary(employee.getId())).withRel("salary"));
}
}
4. Create a employee controller
Create a employee controller for represent the end point for retrieve employee details.
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/employees")
public class EmployeeController {
// Simulate a service or repository
private List<Employee> employees = List.of(
new Employee(1L, "John Doe", "Developer", 5000.0),
new Employee(2L, "Jane Smith", "Manager", 7000.0)
);
// Get employee by ID
@GetMapping("/{id}")
public EntityModel<Employee> getEmployee(@PathVariable Long id) {
Employee employee = employees.stream()
.filter(e -> e.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Employee not found"));
return new EmployeeResource(employee);
}
// Get employee salary
@GetMapping("/{id}/salary")
public EntityModel<Double> getEmployeeSalary(@PathVariable Long id) {
Employee employee = employees.stream()
.filter(e -> e.getId().equals(id))
.findFirst()
.orElseThrow(() -> new RuntimeException("Employee not found"));
// Wrap salary in EntityModel and add links
EntityModel<Double> salaryResource = EntityModel.of(employee.getSalary());
salaryResource.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployeeSalary(id)).withSelfRel());
salaryResource.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployee(id)).withRel("employee"));
return salaryResource;
}
// Get all employees
@GetMapping
public CollectionModel<EntityModel<Employee>> getAllEmployees() {
List<EntityModel<Employee>> employeeResources = employees.stream()
.map(EmployeeResource::new)
.collect(Collectors.toList());
return CollectionModel.of(employeeResources,
WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getAllEmployees()).withSelfRel());
}
}
5. Responses
- Get employee details by Id.
GET /employees/1
{
"id": 1,
"name": "John Doe",
"role": "Developer",
"salary": 5000.0,
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"salary": {
"href": "http://localhost:8080/employees/1/salary"
}
}
}
- Get employees’ salary by id
GET /employees/1/salary
{
"content": 5000.0,
"_links": {
"self": {
"href": "http://localhost:8080/employees/1/salary"
},
"employee": {
"href": "http://localhost:8080/employees/1"
}
}
}
- Get all employees
GET /employees
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "John Doe",
"role": "Developer",
"salary": 5000.0,
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"salary": {
"href": "http://localhost:8080/employees/1/salary"
}
}
},
{
"id": 2,
"name": "Jane Smith",
"role": "Manager",
"salary": 7000.0,
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"salary": {
"href": "http://localhost:8080/employees/2/salary"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}
Conclusion
By integrating HATEOAS into the employee payroll system, we create a self-descriptive, dynamic API where clients can navigate actions like viewing salaries, updating them, or accessing employee details through embedded hypermedia links. This approach decouples clients from hardcoded URLs, enhances discoverability, and ensures flexibility as the API evolves. Spring HATEOAS simplifies link generation, making it easier to build RESTful services that adhere to REST architecture principles while improving scalability and maintainability.
Reference
Mastering REST: The Role of HATEOAS in API Design | Pradeep Loganathan’s Blog
Spring HATEOAS — Reference Documentation