Back to Pagination, Filtering & HATEOAS
Q4Question 4 of 4

Explain HATEOAS and its role in REST APIs, with Spring HATEOAS

Key Concepts
  • HATEOAS means responses include links describing the next possible actions, driven by the resource's current state.
  • Each link has a relation name (rel) and a URL (href); the client branches on the rel to decide what to render.
  • State-driven links mean the server controls valid transitions: an active account includes transfer; a frozen one does not.
  • Spring HATEOAS provides EntityModel, CollectionModel, and linkTo/methodOn for building type-safe links.
  • HAL is the default media type in Spring HATEOAS, representing links under an _links object.
  • The highest value of HATEOAS in banking is preventing the client from duplicating the server's state-transition rules.

Definition

HATEOAS means responses carry links telling the client what actions are possible next from the current state, so clients follow links instead of hardcoding URLs.
  • HATEOAS – Hypermedia As The Engine Of Application State, the REST constraint where responses include links describing available actions.
  • Link – has a relation name (rel) like self, transactions, or freeze, and a URL (href); the client branches on the rel.
  • State-driven – links change with the resource's state: an active account includes freeze and transfer; a frozen account shows unfreeze and hides transfer.
  • Spring HATEOAS – provides EntityModel, CollectionModel, and linkTo/methodOn helpers to build type-safe links from controller methods.
  • HAL – Hypertext Application Language, the default media type for Spring HATEOAS, with an _links object.
  • Benefit – the server controls which actions are valid; the client never replicates business rules about state transitions.

Imagine Rajesh's account statement at National Bank of India comes with a menu at the bottom listing exactly what he can do right now: view transactions, transfer money, freeze the account. If the account is already frozen, the menu shows unfreeze and hides transfer. Rajesh never memorises counter numbers; the statement tells him what is possible. HATEOAS is that self-describing menu, and it changes based on the account's current state.

Understanding the Concepts

What is HATEOAS?

HATEOAS stands for Hypermedia As The Engine Of Application State. It is the REST constraint that says responses should include links describing the actions a client can take next. Instead of hardcoding URLs, the client makes one entry request and follows the links in each response to discover what it can do.

A link has at least a relation name (rel) and a URL (href). The rel tells the client the meaning, like self, transactions, or freeze, and the href is where to go. The client branches on the rel it understands.

JAVA
// HATEOAS response with links:
// GET /api/v1/accounts/SAV2001
// {
//   "accountId": "SAV2001",
//   "balance": 1000000.00,
//   "status": "ACTIVE",
//   "_links": {
//     "self":         {"href": "/api/v1/accounts/SAV2001"},
//     "transactions": {"href": "/api/v1/accounts/SAV2001/transactions"},
//     "transfer":     {"href": "/api/v1/accounts/SAV2001/transfers"},
//     "freeze":       {"href": "/api/v1/accounts/SAV2001/freeze"}
//   }
// }

The crucial idea is that the server includes only the links valid for the resource's current state. An active account includes transfer and freeze; a frozen account includes unfreeze and omits transfer; a closed account includes neither. The client renders buttons for the links present and does not replicate the server's business rules.

JAVA
// Active account:
// _links: { self, transactions, transfer, freeze }

// Frozen account:
// _links: { self, transactions, unfreeze }
// Note: transfer and freeze are ABSENT – client does not show those buttons.

// Closed account:
// _links: { self }
// Only the self link remains.

Spring HATEOAS provides EntityModel (wraps a DTO with links), CollectionModel (wraps a collection), and the linkTo/methodOn helpers that build links from controller methods in a type-safe way. If you rename a path, the link updates automatically.

JAVA
package com.bank.controller;

import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

@RestController
@RequestMapping("/api/v1/accounts")
public class AccountController {

    @GetMapping("/{id}")
    public EntityModel<AccountResponse> get(@PathVariable String id) {
        AccountResponse dto = accountService.findById(id);
        EntityModel<AccountResponse> model = EntityModel.of(dto);

        // Always-available links:
        model.add(linkTo(methodOn(AccountController.class).get(id)).withSelfRel());
        model.add(linkTo(methodOn(TransactionController.class).list(id, null, 20))
                .withRel("transactions"));

        // State-dependent links:
        if (dto.isActive()) {
            model.add(linkTo(methodOn(AccountController.class).freeze(id))
                    .withRel("freeze"));
            model.add(linkTo(methodOn(TransferController.class).create(null, null))
                    .withRel("transfer"));
        } else if (dto.isFrozen()) {
            model.add(linkTo(methodOn(AccountController.class).unfreeze(id))
                    .withRel("unfreeze"));
        }

        return model;
    }
}

What is HAL format?

HAL (Hypertext Application Language) is the default media type for Spring HATEOAS. It represents links under an _links object mapping relation names to href objects. Embedded resources go under _embedded.

JAVA
// HAL structure:
// {
//   "accountId": "SAV2001",
//   "balance": 1000000.00,
//   "_links": {
//     "self":         {"href": "/api/v1/accounts/SAV2001"},
//     "transactions": {"href": "/api/v1/accounts/SAV2001/transactions"}
//   }
// }

How does HATEOAS help with collections?

For collections, CollectionModel wraps a list and adds pagination links like next, prev, and self. This is the one place HATEOAS is widely adopted because pagination links make paging trivial for clients.

JAVA
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.PagedModel;

// Collection with pagination links:
@GetMapping
public CollectionModel<EntityModel<TransactionResponse>> listTransactions(
        @PathVariable String accountId,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size) {
    // ... fetch items ...
    CollectionModel<EntityModel<TransactionResponse>> collection =
            CollectionModel.of(items);
    collection.add(linkTo(methodOn(TransactionController.class)
            .listTransactions(accountId, page + 1, size)).withRel("next"));
    collection.add(linkTo(methodOn(TransactionController.class)
            .listTransactions(accountId, page, size)).withSelfRel());
    return collection;
}

Why does HATEOAS matter for a banking API?

The value of HATEOAS in banking is that the server controls which actions are valid. The mobile app had duplicated the rule that the transfer button shows only for active accounts. When the bank added a "dormant" state that also blocks transfers, the server was updated but the app still showed the button, causing failed transfers. With HATEOAS, the server includes a transfer link only when a transfer is allowed, so the app renders buttons for the links present and never replicates the rule.

JAVA
// Without HATEOAS: client duplicates business rules.
if (account.status == "ACTIVE") { showTransferButton(); }
// Bug: app does not know about new "DORMANT" state.

// With HATEOAS: client follows links.
if (response._links.transfer) { showTransferButton(); }
// Server omits transfer link for DORMANT → app automatically hides button.

What are the other media types for hypermedia?

Beyond HAL, other media types include Collection+JSON, JSON:API, and HAL-FORMS (which can also describe the HTTP method and input fields for each action). HAL is the most common in Spring HATEOAS.

JAVA
// HAL (default in Spring HATEOAS):
// Content-Type: application/hal+json
// _links: { self: {href: "..."} }

// HAL-FORMS (describes method and input):
// _templates: { default: { method: "POST", properties: [...] } }

What are the trade-offs of full HATEOAS?

Full HATEOAS adds complexity on both server and client. The server must compute valid links for every response. Clients must parse and follow links instead of calling known URLs. Payload size grows with link metadata. Most teams adopt HATEOAS selectively: pagination links and state-dependent action links, without full hypermedia navigation.

JAVA
// Selective HATEOAS (recommended):
// 1. Pagination links: next, prev, self → widely useful.
// 2. State-dependent actions: transfer, freeze → prevents rule duplication.
// 3. Skip full hypermedia navigation → clients still use documented URLs.

What should a senior engineer know about HATEOAS?

A senior engineer knows that HATEOAS optimises for long-lived generic clients like web browsers, while most business APIs have purpose-built clients that co-evolve with the server. They apply HATEOAS where state-dependent discoverability genuinely helps and use OpenAPI for the rest. They know that the highest value in a banking API is the state-driven action links, which keep the state machine only on the server.

Visual Overview

DIAGRAM
┌──────────────────────────────────────────────────────────┐
│           HATEOAS with Spring HATEOAS                     │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  Controller:                                             │
│  ┌─────────────────────────────────────────────────┐    │
│  │ AccountResponse dto = service.findById(id);      │    │
│  │ EntityModel<AccountResponse> model =             │    │
│  │     EntityModel.of(dto);                         │    │
│  │ model.add(linkTo(...).withSelfRel());            │    │
│  │ if (dto.isActive()) {                            │    │
│  │     model.add(linkTo(...).withRel("transfer"));  │    │
│  │     model.add(linkTo(...).withRel("freeze"));    │    │
│  │ }                                                │    │
│  │ return model;                                    │    │
│  └─────────────────────────────────────────────────┘    │
│       │                                                  │
│       ▼                                                  │
│  HAL Response:                                           │
│  ┌─────────────────────────────────────────────────┐    │
│  │ {                                                │    │
│  │   "accountId": "SAV2001",                        │    │
│  │   "balance": 1000000.00,                         │    │
│  │   "_links": {                                    │    │
│  │     "self":     {"href": "/accounts/SAV2001"},   │    │
│  │     "transfer": {"href": "/accounts/.../tfrs"},  │    │
│  │     "freeze":   {"href": "/accounts/.../freeze"} │    │
│  │   }                                              │    │
│  │ }                                                │    │
│  └─────────────────────────────────────────────────┘    │
│       │                                                  │
│       ▼                                                  │
│  Client: render buttons for links present.               │
│  No hardcoded business rules about state.                │
└──────────────────────────────────────────────────────────┘

Key Takeaway

HATEOAS stands for Hypermedia As The Engine Of Application State, and it is the REST constraint where responses include links describing the actions a client can take next. Each link has a relation name and a URL, and the client branches on the relation it understands. The key idea is that links are state-driven: the server includes only the links valid for the resource's current state, so an active account carries transfer and freeze links while a frozen account carries unfreeze and omits transfer.

In Spring HATEOAS, you build this with EntityModel wrapping a DTO and the linkTo/methodOn helpers generating links from controller methods in a type-safe way. The default media type is HAL, which represents links under an _links object. You add always-available links like self and transactions, then conditionally add state-dependent links based on the resource's status.

The role of HATEOAS at National Bank of India was keeping business rules on the server. The mobile app had duplicated the rule that the transfer button shows only for active accounts, and when the bank added a dormant state, the server was updated but the app still showed the button, causing confusing failures. With HATEOAS, the server includes a transfer link only when a transfer is allowed, so the app renders buttons for the links present and never replicates the rule. Adding the dormant state corrected every client with no app change.

The honest trade-off is that full HATEOAS adds complexity, and few clients drive themselves entirely from hypermedia. The highest value in a banking API is the state-driven action links, which stop the client from having to know when an account can be frozen or a transfer allowed. That single benefit is usually worth it even when HATEOAS is adopted partially for state-dependent actions and pagination rather than everywhere.

Remember one sentence: HATEOAS embeds state-driven links in responses so the server controls which actions are valid and the client never replicates business rules about state transitions.

---

Share
Use ← → arrow keys to navigate