Skip to content

Spring Boot HATEOAS Restful API | Advantages, Trade Offs with Examples

The Choose Your Own Adventure Book Analogy

Imagine reading a traditional linear novel. On page ten, the story ends with a static paragraph. If you want to know what the hero can do next, you have to read external spoiler guides, look up author interviews online, or guess what page to turn to next.

Now imagine a "Choose Your Own Adventure" interactive game book. When you reach the end of page ten, the text explicitly gives you dynamic choices with page numbers:

  • If you want to enter the dark cave, turn to page 24.
  • If you want to cross the rope bridge, turn to page 38.
  • If you want to inspect your inventory, turn to page 5.

You do not need an external map to navigate the story. The book itself guides your next actions dynamically based on your current state.

In REST API design, HATEOAS (Hypermedia As The Engine Of Application State) is that interactive adventure book. In a standard REST API, when a client fetches an order, the server returns plain data fields (status, amount). The client must consult external API documentation to guess what URLs exist to cancel or pay for that order. With HATEOAS, the server returns the data along with hypermedia links describing exactly what actions the client is currently permitted to take next.

This lecture covers the Richardson Maturity Model, Spring HATEOAS library, EntityModel, building dynamic links with WebMvcLinkBuilder, and evaluating real world trade offs.


The Richardson Maturity Model

Leonard Richardson developed a four level model to evaluate how truly RESTful a web API is:

Level 3: Hypermedia Controls (HATEOAS)  <-- The Glory of REST
Level 2: HTTP Verbs (GET, POST, PUT, DELETE) + Status Codes
Level 1: Distinct Resources (URIs like /orders, /users)
Level 0: The Swamp of POX (Single URI like /api, all POST requests, RPC style)

Most commercial REST APIs operate at Level 2: they use distinct URIs, proper HTTP verbs (GET, POST), and appropriate status codes (200, 201, 404).

Level 3 (HATEOAS) represents the final stage of REST maturity, where responses include hypermedia links describing state transitions.


Standard JSON vs HATEOAS JSON

1. Standard Level 2 Response:

json
{
  "id": "ORD-101",
  "amount": 150.00,
  "status": "PENDING"
}

The client receives the data, but has no programmatic knowledge of how to cancel, pay for, or update this order without hardcoding URLs in the client application code.

2. Level 3 HATEOAS Response (HAL Format):

json
{
  "id": "ORD-101",
  "amount": 150.00,
  "status": "PENDING",
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/orders/ORD-101"
    },
    "payment": {
      "href": "http://localhost:8080/api/orders/ORD-101/pay"
    },
    "cancel": {
      "href": "http://localhost:8080/api/orders/ORD-101/cancel"
    },
    "customer": {
      "href": "http://localhost:8080/api/customers/CUST-99"
    }
  }
}

Notice the power of this response:

  • If the order transitions to CANCELLED, subsequent GET requests will omit the "cancel" and "payment" links. The client frontend can simply inspect the _links object: if a "payment" link exists, render a "Pay Now" button; if it is absent, hide the button.
  • The server drives the client's state transitions dynamically.

Implementing Spring HATEOAS

1. Add Spring HATEOAS Dependency in pom.xml

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

2. Core Model Wrappers

Spring HATEOAS provides generic representation models:

  • EntityModel<T>: Wraps a single domain object and attaches links.
  • CollectionModel<T>: Wraps a list or collection of domain objects and attaches collection level links.

Hardcoding link URLs like "http://localhost:8080/api/orders/" + id is brittle. If you change a controller's @RequestMapping, hardcoded string links break silently.

Spring HATEOAS provides WebMvcLinkBuilder to construct links dynamically by inspecting controller method signatures:

java
package com.example.orderservice.controller;

import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/{id}")
    public ResponseEntity<EntityModel<OrderDto>> getOrderById(@PathVariable Long id) {
        OrderDto order = orderService.findById(id);

        // 1. Wrap domain DTO in EntityModel
        EntityModel<OrderDto> resource = EntityModel.of(order);

        // 2. Build self link pointing to this exact method
        Link selfLink = linkTo(methodOn(OrderController.class).getOrderById(id)).withSelfRel();
        resource.add(selfLink);

        // 3. Build conditional business state links
        if ("PENDING".equals(order.getStatus())) {
            Link payLink = linkTo(methodOn(OrderController.class).payForOrder(id)).withRel("payment");
            Link cancelLink = linkTo(methodOn(OrderController.class).cancelOrder(id)).withRel("cancel");
            resource.add(payLink);
            resource.add(cancelLink);
        }

        return ResponseEntity.ok(resource);
    }

    @PostMapping("/{id}/pay")
    public ResponseEntity<String> payForOrder(@PathVariable Long id) {
        orderService.processPayment(id);
        return ResponseEntity.ok("Order paid successfully");
    }

    @PostMapping("/{id}/cancel")
    public ResponseEntity<String> cancelOrder(@PathVariable Long id) {
        orderService.cancel(id);
        return ResponseEntity.ok("Order cancelled");
    }
}

How WebMvcLinkBuilder Works:

methodOn(OrderController.class).getOrderById(id) creates a dummy proxy invocation. Spring inspects the controller's @RequestMapping and @GetMapping annotations, resolves any path variables, and produces the exact canonical URL with zero hardcoded string literals.


Evaluating HATEOAS: Advantages vs Trade Offs

Advantages:

  1. Self Describing APIs: Clients discover capabilities and permitted state transitions dynamically from the response body.
  2. Decoupled Clients: Clients navigate by relationship names ("payment", "cancel") rather than hardcoded URLs, allowing backend engineers to restructure URL paths without breaking clients.
  3. Dynamic UI Rendering: Frontends can toggle action buttons simply by checking for the presence of link relation keys.

Trade Offs & Drawbacks:

  1. Payload Bloat: Repeating link objects on every item in large collection responses increases JSON payload size by 30% to 50%.
  2. Implementation Complexity: Writing and maintaining link builders across complex domain graphs increases backend boilerplate.
  3. Limited Client Adoption: Many standard frontend mobile and web clients simply ignore hypermedia links and use hardcoded API endpoints regardless.

Interview Questions & Pitfalls

Q1: What does HATEOAS stand for, and which level of the Richardson Maturity Model does it represent?

HATEOAS stands for Hypermedia As The Engine Of Application State. It represents Level 3, the highest level of the Richardson Maturity Model.

Q2: What is the primary difference between EntityModel and CollectionModel in Spring HATEOAS?

EntityModel<T> wraps a single domain object and attaches links relevant to that individual entity (e.g. self, update, delete). CollectionModel<T> wraps a collection of entities and attaches links relevant to the entire collection (e.g. self, pagination next and previous links).

Q3: How does WebMvcLinkBuilder.methodOn prevent brittle URL links?

methodOn uses reflection and dynamic proxies to record a call against the target controller method. It reads the method's @GetMapping, @PostMapping, and @RequestMapping path patterns directly from Java code, automatically generating correct relative or absolute URLs without hardcoding fragile string paths.

Q4: What is a rel (relation) attribute in a hypermedia link?

The rel attribute defines the semantic meaning or relationship of the link to the current resource. Common standard relations include self (points to the canonical URI of the resource itself), next and prev (for pagination), or custom domain relations like cancel and payment.

Q5: Why do many enterprise production APIs choose to stop at Richardson Maturity Model Level 2 rather than adopting HATEOAS?

HATEOAS adds significant JSON payload size overhead, requires substantial controller boilerplate to build dynamic links, and demands sophisticated client side architecture to consume dynamic links. In internal microservice systems where API contracts are already codified in OpenAPI specifications, the added complexity of HATEOAS often outweighs the benefits.