HATEOAS-Driven REST APIs: Complete Spring Boot CRUD Implementation

HATEOAS (Hypermedia as the Engine of Application State) is an advanced REST principle that enhances APIs by including links inside responses, guiding clients on available actions dynamically.

In this guide, we’ll cover:
 ✔ What is HATEOAS?
 ✔ Why Use HATEOAS in REST APIs?
 ✔ How HATEOAS Works (Example JSON Response)
 ✔ Implementing HATEOAS in Spring Boot
 ✔ Best Practices for HATEOAS-Driven APIs

Let’s dive in! 🚀

🔹 What is HATEOAS?

HATEOAS stands for Hypermedia as the Engine of Application State. It is a key constraint of REST that makes APIs self-explanatory by including links inside responses.

💡 Without HATEOAS: The client needs hardcoded endpoints to know how to interact with the API.

💡 With HATEOAS: The API guides the client on what actions are available dynamically.

📌 Example: Traditional REST API (Without HATEOAS)

{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}

📌 Example: HATEOAS-Driven API (With Links)

{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"links": [
{"rel": "self", "href": "/users/1"},
{"rel": "update", "href": "/users/1/update"},
{"rel": "delete", "href": "/users/1/delete"}
]
}

What’s New?

  • The API provides links (href) inside the response.
  • The client doesn’t need hardcoded endpoints — it follows the links dynamically.
  • The client knows what actions are possible (self, update, delete).

🔹 Why Use HATEOAS in REST APIs?

Self-Descriptive API Responses — The API tells clients what actions are available.
 ✅ Reduces Hardcoding — Clients don’t need to memorize API URLs.
 ✅ Improves API Evolution — Servers can change URLs without breaking clients.
 ✅ Better API Discoverability — Clients can navigate APIs like browsing the web.

💡 Example Use Case:
 Imagine a shopping app API:

  • A customer fetches a product → The response includes a "buy" link.
  • The customer adds it to the cart → The response includes a "checkout" link.
  • The customer completes the order → The response includes a "track shipment" link.

🔗 The API guides the client step-by-step without needing hardcoded logic!

🔹 How to Implement HATEOAS in Spring Boot?

Let’s create a complete CRUD implementation for a HATEOAS-Driven REST API in Spring Boot, including:
 ✔ Entity (User)
 ✔ DTO (UserModel)
 ✔ Repository (UserRepository)
 ✔ Service Layer (UserService)
 ✔ Controller (UserController)
 ✔ HATEOAS Model Assembler (UserModelAssembler)
 ✔ Testing CRUD APIs Using Postman

📌 1️⃣ Add Required Dependencies

Add the required dependencies in your pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.2.0</version>
</dependency>

📌 2️⃣ Create the User Entity

import jakarta.persistence.*;

@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;

public User() {}

public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}

📌 3️⃣ Create UserRepository

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {}

📌 4️⃣ Create UserService

import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class UserService {
private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public List<User> getAllUsers() {
return userRepository.findAll();
}

public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}

public User createUser(User user) {
return userRepository.save(user);
}

public User updateUser(Long id, User updatedUser) {
return userRepository.findById(id)
.map(user -> {
user.setName(updatedUser.getName());
user.setEmail(updatedUser.getEmail());
return userRepository.save(user);
}).orElseThrow(() -> new RuntimeException("User not found"));
}

public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}

📌 5️⃣ Create UserModel for HATEOAS

import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.core.Relation;

@Relation(collectionRelation = "users")
public class UserModel extends RepresentationModel<UserModel> {
private Long id;
private String name;
private String email;

public UserModel(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}

📌 6️⃣ Create UserModelAssembler

import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
import org.springframework.stereotype.Component;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

@Component
public class UserModelAssembler extends RepresentationModelAssemblerSupport<User, UserModel> {

public UserModelAssembler() {
super(UserController.class, UserModel.class);
}

@Override
public UserModel toModel(User user) {
UserModel userModel = new UserModel(user.getId(), user.getName(), user.getEmail());

userModel.add(linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel());
userModel.add(linkTo(methodOn(UserController.class).updateUser(user.getId(), user)).withRel("update"));
userModel.add(linkTo(methodOn(UserController.class).deleteUser(user.getId())).withRel("delete"));

return userModel;
}

public CollectionModel<UserModel> toCollectionModel(List<User> users) {
return CollectionModel.of(users.stream().map(this::toModel).toList());
}
}

📌 7️⃣ Create UserController

import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
private final UserModelAssembler userModelAssembler;

public UserController(UserService userService, UserModelAssembler userModelAssembler) {
this.userService = userService;
this.userModelAssembler = userModelAssembler;
}

@GetMapping
public CollectionModel<UserModel> getAllUsers() {
List<User> users = userService.getAllUsers();
return userModelAssembler.toCollectionModel(users);
}

@GetMapping("/{id}")
public ResponseEntity<UserModel> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(userModelAssembler::toModel)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<UserModel> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(userModelAssembler.toModel(createdUser));
}

@PutMapping("/{id}")
public ResponseEntity<UserModel> updateUser(@PathVariable Long id, @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(userModelAssembler.toModel(updatedUser));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}

📌 8️⃣ Test CRUD APIs Using Postman

1️⃣ Create a User (POST /users)

{
"name": "John Doe",
"email": "john@example.com"
}

📌 Response:

{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/users/1" },
"update": { "href": "/users/1/update" },
"delete": { "href": "/users/1/delete" }
}
}

2️⃣ Get All Users (GET /users)
3️⃣ Get a User (GET /users/1)
4️⃣ Update User (PUT /users/1)
5️⃣ Delete User (DELETE /users/1)

Now, you have a fully functional HATEOAS-driven REST API with CRUD operations in Spring Boot! 🚀

🔹 Best Practices for HATEOAS-Driven APIs

Always include a "self" link in responses.
 ✅ Use meaningful "rel" names for actions (update, delete, checkout).
 ✅ Keep API discoverability in mind—clients should navigate without hardcoding.
 ✅ Use @Relation annotations to structure HATEOAS responses properly.
 ✅ Ensure backward compatibility when updating links.

🚀 Final Thoughts

HATEOAS makes REST APIs more discoverable, dynamic, and future-proof. By guiding clients through hypermedia links, APIs become self-explanatory and flexible.

💡 Want to build powerful APIs? Implement HATEOAS and let your APIs tell the client what to do next! 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare