skip to content

How do you assemble self and collection links consistently across endpoints, e.g. with RepresentationModelAssembler and CollectionModel?

level: middleimportance: should knowfreq 35%

answer

  1. RepresentationModelAssembler.toModel(entity)
  2. EntityModel.of(entity, selfLink, relLink)
  3. toCollectionModel → CollectionModel (_embedded + _links)
  4. PagedResourcesAssembler → PagedModel first/prev/next/last
  5. centralize URLs, thin controllers

basics

~10 s

Put link-building in a RepresentationModelAssembler: toModel(entity) returns an EntityModel with linkTo(methodOn(...)).withSelfRel() plus related rels. For lists, wrap items in a CollectionModel and add a self link to the collection endpoint.

solid answer

~30 s

To avoid duplicating link code in every handler, implement RepresentationModelAssembler<T, EntityModel<T>> (or extend RepresentationModelAssemblerSupport). Its toModel(entity) builds one item's links — a self link via linkTo(methodOn(ItemController.class).getItem(entity.id())).withSelfRel() and any related rels via withRel(...). For a list endpoint, call assembler.toCollectionModel(entities) to get a CollectionModel<EntityModel<T>>, then add a self link pointing at the collection method, e.g. linkTo(methodOn(ItemController.class).all()).withSelfRel(). This centralizes URL knowledge, keeps controllers thin, and guarantees every item exposes the same rels. CollectionModel serializes to HAL with _embedded for items and _links for the collection's own links. For paged results, PagedModel adds first/prev/next/last links (typically via a PagedResourcesAssembler).

code

java · 30 lines
java
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

@RestController
@RequestMapping("/items")
class ItemController {
    private final ItemAssembler assembler;
    private final ItemService service;
    // ctor omitted

    @GetMapping("/{id}")
    EntityModel<Item> getItem(@PathVariable Long id) {
        return assembler.toModel(service.find(id));
    }

    @GetMapping
    CollectionModel<EntityModel<Item>> all() {
        var model = assembler.toCollectionModel(service.findAll());
        model.add(linkTo(methodOn(ItemController.class).all()).withSelfRel());
        return model;
    }

    @GetMapping("/paged")
    PagedModel<EntityModel<Item>> paged(
            org.springframework.data.domain.Pageable p,
            PagedResourcesAssembler<Item> pagedAssembler) {
        return pagedAssembler.toModel(service.page(p), assembler); // adds first/prev/next/last
    }
}

go deeper

for a junior

Know you can wrap an entity in EntityModel.of(entity, selfLink) and a list in CollectionModel.

for a middle

Implement RepresentationModelAssembler.toModel and use toCollectionModel plus a collection self link.

for a senior

Add PagedResourcesAssembler/PagedModel for pagination and keep rels uniform across items.

for a principal

Treat the assembler as the single source of URL truth per resource; ensure statelessness, testability, and stable rel contracts.

Building links inline in every controller method leads to duplication and drift. Spring HATEOAS's **assembler** pattern centralizes it. **RepresentationModelAssembler<T, D>** is an interface with `D toModel(T entity)` and a default `CollectionModel<D> toCollectionModel(Iterable<T>)`. Typically `D` is `EntityModel<T>`. You implement `toModel` once to attach that entity's links: ```java @Component class ItemAssembler implements RepresentationModelAssembler<Item, EntityModel<Item>> { public EntityModel<Item> toModel(Item item) { return EntityModel.of(item, linkTo(methodOn(ItemController.class).getItem(item.id())).withSelfRel(), linkTo(methodOn(ItemController.class).all()).withRel("items")); } } ``` Controllers then stay thin: `return assembler.toModel(service.find(id));`. **RepresentationModelAssemblerSupport** is an abstract base that also gives helpers like `createModelWithId(id, entity)` (adds a self link automatically) and manages the controller class + model type. Extend it when you want that boilerplate handled. **Collections.** For a list endpoint, `assembler.toCollectionModel(entities)` maps each entity through `toModel` and wraps them in a **CollectionModel<EntityModel<Item>>**. You then add the collection's own self link: ```java CollectionModel<EntityModel<Item>> model = assembler.toCollectionModel(items); model.add(linkTo(methodOn(ItemController.class).all()).withSelfRel()); ``` In HAL this serializes items under `_embedded` and the collection's links under `_links`. **Paging.** For `Page<T>`, use **PagedResourcesAssembler<T>** (inject it as a controller parameter). `pagedAssembler.toModel(page, itemAssembler)` returns a **PagedModel** with a `page` metadata block and standard navigation links — `self`, `first`, `prev`, `next`, `last` (using IanaLinkRelations semantics) — computed from the page number/size. This is the idiomatic way to expose pagination as hypermedia rather than hand-rolling next/prev. **Why this matters:** all URL knowledge for a resource lives in one class, so a mapping change updates every representation at once; every item exposes a uniform set of rels; and controllers focus on orchestration. It also makes link logic **unit-testable** in isolation (with a mock request). **Gotchas:** the assembler still calls `linkTo`, so it inherits the request-context requirement; keep it stateless/`@Component` singleton-safe; and be consistent about which rels every item exposes so clients can rely on them.

  • What does PagedResourcesAssembler add that a plain CollectionModel does not?
    It produces a PagedModel with page metadata (size, totalElements, totalPages, number) and standard navigation links — self, first, prev, next, last — computed from the Page, so pagination is expressed as hypermedia automatically.

saying these in an interview costs you the question

  • Duplicating linkTo calls in every controller method instead of centralizing in an assembler.
  • Thinking CollectionModel embeds items under _links rather than _embedded.
  • Believing the assembler escapes the request-context requirement — it still calls linkTo.

context