Spring HATEOAS
SkillSearchUse 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Spring HATEOAS skill
What this skill tells your AI
The instructions your AI receives, as published by rrezartprebreza/spring-boot-skills in skills/spring-boot-3/hateoas/SKILL.md and read by ahel’s review.
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
When to Add Links
self— always, on every resource responsecollection— link back to the list endpointrelated resources— when a client commonly needs to navigate to themactions— links to state transitions (e.g.,cancel,ship) when valid for current state
Resource Model
public class OrderModel extends RepresentationModel<OrderModel> {
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
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/{id}")
public ResponseEntity<OrderModel> getById(@PathVariable UUID id) {
Order order = orderService.findById(id);
return ResponseEntity.ok(OrderModel.from(order));
}
@GetMapping
public ResponseEntity<CollectionModel<OrderModel>> list(Pageable pageable) {
Page<Order> orders = orderService.findAll(pageable);
List<OrderModel> models = orders.getContent().stream()
.map(OrderModel::from)
.toList();
CollectionModel<OrderModel> 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
{
"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<Entity, Model>— reusable across controllers - Inject the assembler into controllers instead of calling
Model.from()directly
@Component
public class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<OrderResponse>> {
@Override
public EntityModel<OrderResponse> toModel(Order order) {
EntityModel<OrderResponse> 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
PagedResourcesAssemblerfor automatic pagination links (first, prev, next, last) - Inject
PagedResourcesAssembler<Order>into controllers — Spring creates it automatically
@GetMapping
public ResponseEntity<PagedModel<EntityModel<OrderResponse>>> list(
Pageable pageable, PagedResourcesAssembler<Order> pagedAssembler) {
Page<Order> orders = orderService.findAll(pageable);
PagedModel<EntityModel<OrderResponse>> 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 extendRepresentationModel - Agent puts link logic in controller — extract to
RepresentationModelAssembler - Agent manually builds pagination links — use
PagedResourcesAssemblerinstead - Agent forgets
selflink — every resource must have aselflink
Signals
- GitHub stars
- 260
- Forks
- 40
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
hateoas- Source
- github.com/rrezartprebreza/spring-boot-skills