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
Post a Comment
Leave Comment