Mastering HATEOAS with Spring Boot: Elevating REST APIs

Introduction

RESTful APIs are often treated as fixed contracts, with clients relying on predefined URLs and response formats, however, this approach creates tight coupling between client and server, making the system harder to adapt or evolve without breaking existing integrations.

Here’s where HATEOAS comes into play.

Instead of assuming fixed URLs and response structures, HATEOAS lets the server embed actionable links directly into the response, these links guide the client on what can be done next, like retrieving a single category, navigating pages, or updating a resource, without the client needing to know endpoint details in advance.

By following these links, the client becomes decoupled from the server’s internal URL structure, changes on the backend like renaming endpoints or adding new capabilities don’t break existing clients, as long as the link semantics remain consistent.

In essence, HATEOAS makes your API self-descriptive and future-proof, enabling a more dynamic and resilient interaction model between client and server.

The Richardson Maturity Model: Understanding REST in Levels


Article content

To understand where HATEOAS fits in the REST landscape, we need to look at the Richardson Maturity Model (RMM) a simple framework created by Leonard Richardson to evaluate how "RESTful" an API truly is.

The model defines four levels, each representing a deeper adherence to REST principles:

Level 0 – The Swamp of POX

At this level, APIs expose a single endpoint typically using POST with all operations funneled through it. There's no concept of resources or HTTP methods. It’s basically RPC over HTTP.

Level 1 – Resources

This level introduces resource-oriented URIs., instead of /doAction, you now have endpoints like /products or /orders, making it easier to reason about the system's structure.

Level 2 – HTTP Verbs

Here, the API starts using HTTP methods properly:

  • GET to fetch data
  • POST to create
  • PUT or PATCH to update
  • DELETE to remove

Most REST APIs today stop at this level and it’s generally enough for many use cases.

Level 3 – Hypermedia Controls (HATEOAS)

This is the highest level of maturity, APIs at this level include hypermedia links in responses to describe what actions can be performed next. The client doesn’t need to construct URLs it simply follows the links provided.

Implementing HATEOAS in GET /categories using TDD


We followed a TDD approach to implement the HATEOAS-enabled getAllCategories() endpoint in our microservice, here’s how we built it starting from a failing test, and evolving toward a fully functional, hypermedia-driven response.

Step 1: Write the Test First

We began by writing an integration test that described the desired behavior:

  • The endpoint should return a paginated list of categories.
  • Each item should include a self link.
  • The top-level response should include pagination links like self, next, etc.


@Test
void shouldReturnPaginatedCategoriesWithHateoasLinks() throws JsonProcessingException {
        // Arrange
        String url = productEndpoint + "?page=0&size=2";

        // Act
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);

        // Assert
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        var root = mapper.readTree(response.getBody());

        
        assertThat(root.has("_embedded")).isTrue();
        assertThat(root.path("_embedded").has("categoryDTOList")).isTrue();
        assertThat(root.has("_links")).isTrue();
        assertThat(root.path("page").get("size").asInt()).isEqualTo(2);

        var firstCategory = root.path("_embedded").path("categoryDTOList").get(0);
        assertThat(firstCategory.has("name")).isTrue();
        assertThat(firstCategory.has("_links")).isTrue();
        assertThat(firstCategory.path("_links").has("self")).isTrue();
    }        


Step 2: Add the HATEOAS Dependency

We added the Spring HATEOAS dependency in the pom.xml to enable hypermedia support:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>        

Step 3: Implement the Assembler

We created a CategoryModelAssembler that adds self, delete, and update links to each category:

public EntityModel<CategoryDTO> toModel(CategoryDTO categoryDTO) {
        return EntityModel.of(
                categoryDTO,
                linkTo(methodOn(CategoryController.class).getCategory(categoryDTO.name())).withSelfRel(),
                linkTo(methodOn(CategoryController.class).deleteCategory(categoryDTO.name())).withRel(DELETE.rel()),
                linkTo(methodOn(CategoryController.class).updateCategory(categoryDTO.name(), new UpdateCategoryDTO(categoryDTO.description()))).withRel(UPDATE.rel())
        );
}        

The link relations ("self", "delete", "update") are standardized using an enum for clarity and consistency.

Step 4: Make the Test Pass

We updated the controller to return a PagedModel using PagedResourcesAssembler:

@GetMapping
public ResponseEntity<PagedModel<EntityModel<CategoryDTO>>> getAllCategories(
            @RequestParam(defaultValue = "0") @Min(0) int page,
            @RequestParam(defaultValue = "10") @Min(1) int size,
            PagedResourcesAssembler<CategoryDTO> assembler) {

        Pageable pageable = PageRequest.of(page, size);
        Page<CategoryDTO> result = categoryService.getAllCategories(pageable);
        return ResponseEntity.ok(assembler.toModel(result, categoryModelAssembler));
    }        

Step 5: Confirm with Test Results

Once the implementation was complete, we re-ran the test and verified that:

  • Pagination structure is included
  • Each category has a HATEOAS self link
  • The response format is consistent and future-proof

Wrapping Up

We implemented a HATEOAS-compliant GET /categories endpoint using TDD, added pagination, and enriched responses with hypermedia links. The service is tested, structured, and Docker-ready.

What’s Next?

In the next article, we’ll deploy it to AWS, taking it from local container to a live, cloud-hosted microservice.


You can find the full source code in: https://github.com/eofe/product-catalog-service

To view or add a comment, sign in

More articles by Hamza A.

Others also viewed

Explore content categories