Skip to content
alyn
rrezartprebreza

Hateoas skill

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.

$npx skills add rrezartprebreza/spring-boot-skills --skill hateoas
View this skill on GitHubrrezartprebreza/spring-boot-skills/skills/spring-boot-3/hateoas/SKILL.md
Pack stars
353
Pack installs
3k
Alyn Score
55/99
Updated
4 h ago
License
MIT
– views – installs via Alyn

rrezartprebreza

Hateoas

Install Hateoas

Pick your agent. Every command is ready to paste.

$npx skills add rrezartprebreza/spring-boot-skills --skill hateoas

The open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.

01

Install it

Run the command above, or upload the folder in Claude.

02

Ask naturally

Describe the task. Your agent loads hateoas when the request matches its description.

03

Name it if needed

If it does not trigger, say “use the hateoas skill”. In Codex, type $hateoas.

The skill itself

SKILL.md

FolderGitHub

Spring HATEOAS

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
  • 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

@Getter // Jackson serializes through getters — without them the body is only "_links"
@AllArgsConstructor(access = AccessLevel.PRIVATE)
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 PagedResourcesAssembler for 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 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

Tips for Hateoas

  • • Start a fresh session after installing so your agent picks up the new skill.
  • • It bundles reference files. The agent reads them only when needed, so they cost no context until then.
  • • It is part of a pack with 30 skills. Install only the ones you need with --skill to keep triggers precise.
  • • If two skills overlap, mention the one you want by name.

Questions about Hateoas

What does the Hateoas skill do?

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.

How do I install Hateoas?

Run "npx skills add rrezartprebreza/spring-boot-skills --skill hateoas" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.

Does Hateoas work in Cursor and Codex?

Yes. Hateoas uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.

Is Hateoas free?

Hateoas is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.

Who made Hateoas?

Hateoas is published by rrezartprebreza in the rrezartprebreza/spring-boot-skills repository, together with 29 other skills.

Similar skills. From other creators.