Spring Boot is a robust framework for building Java-based applications. When working with RESTful APIs, one of the modern approaches to enhancing usability and providing navigability is HATEOAS (Hypermedia as the Engine of Application State). This tutorial will guide developers through the essentials of implementing HATEOAS with Spring Boot, making your APIs more intuitive and self-descriptive.

1. What is HATEOAS?
HATEOAS is a concept within REST API architecture that enables clients to interact dynamically with the application solely based on hypermedia provided by the server. It makes REST APIs self-documenting by embedding links within responses, guiding the client on what actions are available next.
Example: Traditional vs. HATEOAS
Traditional API Response:
{ "id": 1, "name": "John Doe" }
HATEOAS API Response:
{ "id": 1, "name": "John Doe", "_links": { "self": { "href": "http://api.example.com/users/1" }, "all-users": { "href": "http://api.example.com/users" } } }
2. Setting Up a Spring Boot Project
Initial Setup
1. Navigate to Spring Initializr.
2. Select the following:
- Project: Maven
- Language: Java
- Dependencies: Spring Web, Spring HATEOAS, Spring Data JPA, and H2 Database.
3. Download the project and open it in your favorite IDE.
3. Adding Spring HATEOAS Dependency
If developers are not using Spring Initializr, include the following dependency in your pom.xml
:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
Run mvn clean install
to update your dependencies.
4. Creating a Basic Entity and Repository
User Model
Create a class representing your data model:
@Getter @Setter public class User { private Long id; private String name; private Map<String, String> links = new HashMap<>(); public void addLink(String rel, String href) { links.put(rel, href); } }
5. Implementing HATEOAS in Controllers
5.1 Creating the Controller ResponseEntity
Create a REST controller that returns HATEOAS responses.
@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = new User(); user.setId(id); user.setName("John"); user.addLink("self", linkTo(methodOn(UserController.class).getUserById(id)).toUri().toString()); user.addLink("all-users", linkTo(methodOn(UserController.class).getAllUsers()).toUri().toString()); return ResponseEntity.ok(user); } @GetMapping public CollectionModel<User> getAllUsers() { List<User> users = new ArrayList<>(); User mockUser = new User(); mockUser.setId(1L); mockUser.setName("John"); users.add(mockUser); mockUser = new User(); mockUser.setId(2L); mockUser.setName("Sara"); users.add(mockUser); users.forEach(user -> user.addLink("self", linkTo(methodOn(UserController.class).getUserById(user.getId())).toUri().toString())); return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel()); } }
Explanation
ReponseEntity
: Wraps theUser
entity and adds links to it.CollectionModel
: Encapsulates a collection ofEntityModel
instances.- Hypermedia Links:
linkTo
andmethodOn
: Used to generate URLs based on the controller method dynamically.
5.2 Creating the Controller Entity Model
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private UserProfileRepository userProfileRepository; @GetMapping("/profile/{id}") public EntityModel<UserProfile> getUserById(@PathVariable String id) { UUID uuid = UUID.fromString(id); UserProfile user = userProfileRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); return EntityModel.of(user, linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel(), linkTo(methodOn(UserController.class).getAllUsersProfile()).withRel("all-users")); } @GetMapping("/profile") public CollectionModel<EntityModel<UserProfile>> getAllUsersProfile() { List<EntityModel<UserProfile>> users = userProfileRepository.findAll().stream() .map(user -> EntityModel.of(user, linkTo(methodOn(UserController.class).getUserById(user.getId().toString())).withSelfRel())) .toList(); return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel()); } }
@Data @Entity @Table(name = "user_profiles", schema = "public") public class UserProfile { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @Column(name = "first_name", nullable = false) private String firstName; @Column(name = "last_name", nullable = false) private String lastName; @Column(name = "email", nullable = false, unique = true) private String email; @Column(name = "birth_date", nullable = false) private LocalDateTime birthDate; @Column(name = "sex", nullable = false) private String sex; @Column(name = "create_by", nullable = false) private UUID createBy; @Column(name = "create_date", nullable = false) private LocalDateTime createDate; @Column(name = "update_by") private String updateBy; @Column(name = "update_date") private LocalDateTime updateDate; }
@Repository public interface UserProfileRepository extends JpaRepository<UserProfile, UUID> { }
Set up a PostgreSQL Database.




6. Testing the API
Test Endpoints
Start your application and access the following endpoints using tools like Postman or cURL.
1. GET User by ID: /api/users/1
{ "id": 1, "name": "John", "links": { "all-users": "http://localhost:8080/api/users", "self": "http://localhost:8080/api/users/1" } }
2. GET All Users: /api/users
{ "_embedded": { "userList": [ { "id": 1, "name": "John", "links": { "self": "http://localhost:8080/api/users/1" } }, { "id": 2, "name": "Sara", "links": { "self": "http://localhost:8080/api/users/2" } } ] }, "_links": { "self": { "href": "http://localhost:8080/api/users" } } }
3. GET User profile by ID:
/api/users/profile/1e7a3085-8d0c-453f-b3d2-d1283557a34d
{ "id": "1e7a3085-8d0c-453f-b3d2-d1283557a34d", "firstName": "Marshall", "lastName": "Sweeney", "email": "marshall.s@example.com", "birthDate": "1985-06-06T00:00:00", "sex": "1", "createBy": "454c96c6-dde2-4a62-99f8-54d8837d4791", "createDate": "2024-06-04T16:25:09.507094", "updateBy": null, "updateDate": null, "_links": { "self": { "href": "http://localhost:8080/api/users/profile/1e7a3085-8d0c-453f-b3d2-d1283557a34d" }, "all-users": { "href": "http://localhost:8080/api/users/profile" } } }
4. GET All User profiles:/api/users/profile
{ "_embedded": { "userProfileList": [ { "id": "43c1e74f-f08b-4477-b178-2ea5cdadf33b", "firstName": "Carroll", "lastName": "Sosa", "email": "carroll.s@example.com", "birthDate": "1985-06-06T00:00:00", "sex": "2", "createBy": "d316b5ad-16d7-4187-9b72-8d8cbdd93136", "createDate": "2024-06-04T16:22:17.043636", "updateBy": null, "updateDate": null, "_links": { "self": { "href": "http://localhost:8080/api/users/profile/43c1e74f-f08b-4477-b178-2ea5cdadf33b" } } }, { "id": "81f867c4-86e9-47e6-9f50-bbf22dfe13eb", "firstName": "Cathleen", "lastName": "Mccall", "email": "cathleen.m@example.com", "birthDate": "1985-06-06T00:00:00", "sex": "2", "createBy": "4d4f65ee-cfe3-4431-88e4-b4142eccb739", "createDate": "2024-06-04T16:26:39.325342", "updateBy": null, "updateDate": null, "_links": { "self": { "href": "http://localhost:8080/api/users/profile/81f867c4-86e9-47e6-9f50-bbf22dfe13eb" } } }, { "id": "1e7a3085-8d0c-453f-b3d2-d1283557a34d", "firstName": "Marshall", "lastName": "Sweeney", "email": "marshall.s@example.com", "birthDate": "1985-06-06T00:00:00", "sex": "1", "createBy": "454c96c6-dde2-4a62-99f8-54d8837d4791", "createDate": "2024-06-04T16:25:09.507094", "updateBy": null, "updateDate": null, "_links": { "self": { "href": "http://localhost:8080/api/users/profile/1e7a3085-8d0c-453f-b3d2-d1283557a34d" } } }, { "id": "e3542787-97cd-42de-9608-9fb8d314d2ed", "firstName": "Dan", "lastName": "Marks", "email": "dan.m@example.com", "birthDate": "1985-06-06T00:00:00", "sex": "1", "createBy": "61cd4332-faae-41de-a77c-72f59d625d4f", "createDate": "2024-07-02T11:35:25.341531", "updateBy": null, "updateDate": null, "_links": { "self": { "href": "http://localhost:8080/api/users/profile/e3542787-97cd-42de-9608-9fb8d314d2ed" } } } ] }, "_links": { "self": { "href": "http://localhost:8080/api/users" } } }
JSON Crack
The developer can use JSON Crack to visualize JSON data.


7. Benefits of HATEOAS
- Discoverability: Clients can discover available actions dynamically without external documentation.
- Reduced Client Coupling: Links within responses guide clients, reducing the need for hardcoding URLs.
- Scalability: Makes APIs more flexible to changes.
8. Conclusion
Implementing HATEOAS in Spring Boot enhances REST APIs by making them more navigable and intuitive. Embedding links in responses provides a richer and more interactive experience for API consumers. Start experimenting with HATEOAS today to modernize your API design!