HATEOAS nell'API di HabitQuest

In Spring, il principio HATEOAS(Hypermedia As The Engine Of Application State) viene implementato tramite la libreria Spring HATEOAS, che mette a disposizione EntityModel (risorsa singola con link) e CollectionModel (collezione di risorse con link), costruiti tramite WebMvcLinkBuilder. In questo modo il client non ha bisogno di conoscere a priori la struttura delle URL, ma le scopre dinamicamente dalla risposta stessa. Tutti i REST Controllers usano HATEOAS per rendere l'API navigabile e auto-descrittiva, qui di seguito come esempio è mostrato solo l'AvatarController.

Di conseguenza il client potenzialmente può non contenere URL hardcoded. Se il server sposta o rinomina un endpoint, basta aggiornare i link nelle risposte: i client che navigano correttamente l'ipermedia si adattano senza modifiche. Questo è particolarmente utile in un'architettura a microservizi dove gli endpoint possono evolversi indipendentemente.

Un client può navigare l'intera API a partire da un singolo punto di accesso (/avatars/{id}), scoprendo progressivamente risorse e operazioni. I link non sono solo navigazione passiva: indicano anche quali azioni sono disponibili in un dato momento. Una risorsa health che espone heal e damage comunica implicitamente che queste sono le operazioni legali su di essa.

Costruire i link tramite WebMvcLinkBuilder e methodOn() garantisce che gli URL nei link siano sempre allineati alle annotazioni @RequestMapping del controller. Non c'è rischio di link rotti per refactoring: se si cambia il path di un metodo, il link si aggiorna automaticamente.

Creazione dell'avatar (POST /api/v1/avatars)

Alla creazione, la risposta include immediatamente i link alle principali risorse figlio, così il client sa cosa può fare subito dopo:

EntityModel.of(
    body,
    selfLink(id.value()),
    linkTo(methodOn(AvatarController.class).getAvatar(id.value())).withRel("avatar"),
    linkTo(methodOn(AvatarController.class).getLevel(id.value())).withRel("level"),
    linkTo(methodOn(AvatarController.class).getHealth(id.value())).withRel("health")
);

Dettaglio avatar (GET /api/v1/avatars/{id})

È il punto di accesso principale e restituisce il set di link più ricco: tutte le risorse navigabili a partire dall'avatar.

EntityModel.of(
    dto,
    selfLink(id),
    linkTo(...getInventory(id)).withRel("inventory"),
    linkTo(...getEquippedItems(id)).withRel("equippedItems"),
    linkTo(...getStats(id)).withRel("stats"),
    linkTo(...getLevel(id)).withRel("level"),
    linkTo(...getHealth(id)).withRel("health"),
    linkTo(...getMana(id)).withRel("mana"),
    linkTo(...getMoney(id)).withRel("money"),
    linkTo(...deleteAvatar(id)).withRel("delete")
);

Questa risposta funge da hub di navigazione: un client che conosce solo l'endpoint /avatars/{id} è in grado di raggiungere l'intera API dell'avatar senza documentazione aggiuntiva.

Risorse con operazioni contestuali

Alcune sottorisorse includono i link alle operazioni che su di esse hanno senso, rendendo la risposta auto-descrittiva rispetto allo stato corrente. Denaro — espone direttamente le due operazioni disponibili:

linkTo(...earnMoney(id, null)).withRel("earn"),
linkTo(...spendMoney(id, null)).withRel("spend")

Inventario — collega le operazioni di modifica e la navigazione verso gli oggetti equipaggiati:

linkTo(...addItem(id, null)).withRel("addItem"),
linkTo(...removeItem(id, null)).withRel("removeItem"),
linkTo(...getEquippedItems(id)).withRel("equippedItems")

Salute — espone cura e danno come azioni navigabili:

linkTo(...healAvatar(id, null)).withRel("heal"),
linkTo(...applyDamage(id, null)).withRel("damage")

Statistiche — fornisce i link ai tre potenziamenti disponibili:

linkTo(...increaseStrength(id)).withRel("increaseStrength"),
linkTo(...increaseDefense(id)).withRel("increaseDefense"),
linkTo(...increaseIntelligence(id)).withRel("increaseIntelligence")

Collezioni (GET /api/v1/avatars/search)

Anche le risposte collettive usano HATEOAS. Ogni elemento della lista include un link a se stesso e al proprio avatar, e l'intera collezione porta il link self che identifica la query che l'ha prodotta:

CollectionModel.of(
    avatarModels,
    linkTo(methodOn(AvatarController.class).searchAvatars(query)).withSelfRel()
);

Struttura di una risposta HATEOAS

Una risposta tipica per GET /api/v1/avatars/{id}/money si presenta così:

{
  "amount": 150,
  "_links": {
    "self": { "href": "/api/v1/avatars/abc123" },
    "avatar": { "href": "/api/v1/avatars/abc123" },
    "earn": { "href": "/api/v1/avatars/abc123/money/earn" },
    "spend": { "href": "/api/v1/avatars/abc123/money/spend" }
  }
}

Vantaggi del pattern