---
name: hateoas
description: >
Use when adding hypermedia links to REST responses, building self-describing APIs,
or implementing Spring HATEOAS. Use when you see EntityModel, CollectionModel, or
RepresentationModel in the project.
---
# Spring HATEOAS
## Dependency
```xml
org.springframework.boot
spring-boot-starter-hateoas
```
## When to Add Links
- `self` — always, on every resource response
- `collection` — link back to the list endpoint
- `related resources` — when a client commonly needs to navigate to them
- `actions` — links to state transitions (e.g., `cancel`, `ship`) when valid for current state
## Resource Model
```java
public class OrderModel extends RepresentationModel {
private final UUID id;
private final String status;
private final String customerEmail;
private final Instant createdAt;
// Static factory with links
public static OrderModel from(Order order) {
OrderModel model = new OrderModel(
order.getId(), order.getStatus().name(),
order.getCustomerEmail(), order.getCreatedAt()
);
// Self link — always
model.add(linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel());
// Collection link
model.add(linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));
// Conditional action links based on state
if (order.getStatus() == OrderStatus.PENDING) {
model.add(linkTo(methodOn(OrderController.class)
.cancelOrder(order.getId())).withRel("cancel"));
}
if (order.getStatus() == OrderStatus.PROCESSING) {
model.add(linkTo(methodOn(OrderController.class)
.shipOrder(order.getId())).withRel("ship"));
}
return model;
}
}
```
## Controller
```java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/{id}")
public ResponseEntity getById(@PathVariable UUID id) {
Order order = orderService.findById(id);
return ResponseEntity.ok(OrderModel.from(order));
}
@GetMapping
public ResponseEntity> list(Pageable pageable) {
Page orders = orderService.findAll(pageable);
List models = orders.getContent().stream()
.map(OrderModel::from)
.toList();
CollectionModel collection = CollectionModel.of(models,
linkTo(methodOn(OrderController.class).list(pageable)).withSelfRel()
);
// Pagination links
if (orders.hasNext()) {
collection.add(linkTo(methodOn(OrderController.class)
.list(pageable.next())).withRel(IanaLinkRelations.NEXT));
}
if (orders.hasPrevious()) {
collection.add(linkTo(methodOn(OrderController.class)
.list(pageable.previousOrFirst())).withRel(IanaLinkRelations.PREV));
}
return ResponseEntity.ok(collection);
}
}
```
## Response Shape
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "PENDING",
"customerEmail": "user@example.com",
"_links": {
"self": { "href": "http://api.example.com/api/v1/orders/550e8400" },
"orders": { "href": "http://api.example.com/api/v1/orders" },
"cancel": { "href": "http://api.example.com/api/v1/orders/550e8400/cancel" }
}
}
```
## RepresentationModelAssembler Pattern
- Spring's recommended way to build HATEOAS models from entities
- Implements `RepresentationModelAssembler` — reusable across controllers
- Inject the assembler into controllers instead of calling `Model.from()` directly
```java
@Component
public class OrderModelAssembler implements RepresentationModelAssembler> {
@Override
public EntityModel toModel(Order order) {
EntityModel model = EntityModel.of(OrderResponse.from(order),
linkTo(methodOn(OrderController.class).getById(order.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).list(null)).withRel("orders"));
if (order.getStatus() == OrderStatus.PENDING) {
model.add(linkTo(methodOn(OrderController.class)
.cancelOrder(order.getId())).withRel("cancel"));
}
return model;
}
}
```
## PagedModel for Paginated Collections
- Use `PagedResourcesAssembler` for automatic pagination links (first, prev, next, last)
- Inject `PagedResourcesAssembler` into controllers — Spring creates it automatically
```java
@GetMapping
public ResponseEntity>> list(
Pageable pageable, PagedResourcesAssembler pagedAssembler) {
Page orders = orderService.findAll(pageable);
PagedModel> pagedModel =
pagedAssembler.toModel(orders, orderModelAssembler);
return ResponseEntity.ok(pagedModel);
}
```
## Gotchas
- Agent adds all links regardless of state — only add action links when the action is valid
- Agent hardcodes URLs in links — always use `linkTo(methodOn(...))` for type-safe links
- Agent returns plain DTO — wrap in `EntityModel.of(dto, links...)` or extend `RepresentationModel`
- Agent puts link logic in controller — extract to `RepresentationModelAssembler`
- Agent manually builds pagination links — use `PagedResourcesAssembler` instead
- Agent forgets `self` link — every resource must have a `self` link