Що таке HATEOAS
Простими словами: сервер повертає клієнту не лише «сухі» дані, але й карту допустимих подальших дій у вигляді посилань (URIs).
🟢 Junior Level
HATEOAS (Hypermedia As The Engine Of Application State) — це обмеження архітектурного стилю REST, згідно з яким клієнт взаємодіє з мережевим застосунком виключно через гіпермедіа (посилання), які сервер динамічно надає у відповідях.
Простими словами: сервер повертає клієнту не лише «сухі» дані, але й карту допустимих подальших дій у вигляді посилань (URIs).
Аналогія з веббраузером
Коли ви користуєтеся інтернет-банком через браузер:
- Ви не вводите в адресний рядок URL
/accounts/101/transfer-fundsвручну. - Ви бачите залишок на рахунку і натискаєте кнопку з посиланням «Переказати кошти».
- Якщо у вас немає прав на переказ або рахунок заморожено, кнопка (посилання) просто відсутня на сторінці.
HATEOAS переносить цей самий принцип людино-машинної взаємодії вебсайтів на рівень програмної взаємодії між сервером та API-клієнтом.
Порівняння відповідей API
1. Стандартний REST (Level 2):
{
"orderId": 501,
"status": "AWAITING_PAYMENT",
"amount": 2500
}
Мінус: Клієнт зобов’язаний заздалегідь жорстко захардкодити у своєму коді URL для оплати (/api/v1/orders/501/payments) та самостійно перевіряти, у яких статусах оплата дозволена.
2. REST з підтримкою HATEOAS (Level 3 HAL):
{
"orderId": 501,
"status": "AWAITING_PAYMENT",
"amount": 2500,
"_links": {
"self": { "href": "/api/v1/orders/501" },
"payment": { "href": "/api/v1/orders/501/payments", "type": "POST" },
"cancellation": { "href": "/api/v1/orders/501/cancellations", "type": "POST" }
}
}
Плюс: Клієнт перевіряє наявність ключа payment в об’єкті _links. Якщо замовлення вже оплачено, сервер просто не поверне посилання payment, і клієнт автоматично приховає кнопку оплати.
🟡 Middle Level
Концепція можливостей (Affordances) та State Machine
У HATEOAS посилання представляють собою Affordances (доступні можливості):
- Сервер виступає в ролі скінченного автомата (State Machine).
- Переходи між станами бізнес-сутності визначаються сервером, а не клієнтом.
- Керування доступом (RBAC/ABAC): Якщо в поточного користувача немає ролі адміністратора, посилання
"delete": { "href": "..." }просто не включається у відповідь. Клієнту не потрібно дублювати складну матрицю прав доступу в коді мобільного чи вебдодатка: достатньо перевіритиif (links.delete) renderDeleteButton().
Формати гіпермедіа: HAL та JSON:API
Для стандартизації структури посилань у REST API були розроблені спеціалізовані специфікації:
1. HAL (Hypertext Application Language) — application/hal+json
Найпопулярніший і найкомпактніший формат в екосистемі Java та Spring:
- Кореневе поле
_linksмістить словник відношень (rel) та посилань (href). - Поле
_embeddedмістить вкладені колекції ресурсів (щоб уникнути окремих HTTP-запитів).
2. JSON:API — application/vnd.api+json
Більш суворий стандарт, що регламентує структуру об’єктів data, attributes, relationships та links.
Проблема роботи за Reverse Proxy (Forwarded Headers)
У мікросервісній архітектурі Spring Boot сервіс зазвичай розташований за балансувальником навантаження або API Gateway (NGINX, Envoy, Kubernetes Ingress):
- Сервер генерує посилання на основі свого локального контексту:
http://10.244.1.15:8080/orders/501. - Якщо відправити такий URL клієнту, перехід за посиланням завершиться помилкою мережі.
Рішення:
- Проксі-сервер зобов’язаний проставляти стандартні заголовки RFC 7239:
X-Forwarded-Host: api.example.comX-Forwarded-Proto: httpsX-Forwarded-Port: 443X-Forwarded-Prefix: /api/v1
- У Spring Boot необхідно активувати фільтр:
server.forward-headers-strategy=FRAMEWORK(реєструє
ForwardedHeaderFilter, який автоматично підміняє хост і схему уWebMvcLinkBuilder).
🔴 Senior Level
Чому індустрія практично відмовилася від HATEOAS?
На співбесідах рівня Senior важливо розуміти не лише теоретичні ідеї Роя Філдінга, але й прагматичні причини, чому в 95% реальних проєктів HATEOAS не використовується:
- Невідповідність сучасному Frontend/Mobile UI:
Сучасні SPA (React, Angular) та мобільні додатки (iOS/Android) створюються на основі жорстких макетів інтерфейсу (Figma). Розробник інтерфейсу не може довільно перебудовувати екран лише тому, що з сервера надійшов новий невідомий
rel. - Роздування обсягу переданих даних (Payload Overhead): Для одиничного об’єкта оверхед становить 30–50%. Для списків із сотень елементів (колекцій) обсяг JSON збільшується в 2–3 рази виключно через дублювання URL-рядків у кожному об’єкті.
- Навантаження на CPU (Reflection Overhead):
Динамічна генерація посилань у
RepresentationModelAssemblerчерезWebMvcLinkBuilderвикористовує рефлексію та резолвінг методів контролерів, що створює помітне навантаження за високого RPS. - Перемога OpenAPI / TypeScript Generator: Клієнтським розробникам значно зручніше згенерувати суворо типізований SDK за схемою OpenAPI 3.1, ніж парсити динамічний граф посилань у runtime.
Практична реалізація на Spring HATEOAS (Spring Boot 3)
Для якісної архітектури розділення доменних моделей і HATEOAS-представлень використовується RepresentationModelAssembler:
package com.example.dto;
import com.example.controller.OrderController;
import com.example.entity.Order;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Component
public class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<OrderDto>> {
@Override
public EntityModel<OrderDto> toModel(Order order) {
OrderDto dto = new OrderDto(order.getId(), order.getStatus(), order.getTotalAmount());
// Базове посилання на самого себе (self)
EntityModel<OrderDto> orderModel = EntityModel.of(dto,
linkTo(methodOn(OrderController.class).getOrderById(order.getId())).withSelfRel()
);
// Динамічне додавання посилань на основі стану скінченного автомата
if ("AWAITING_PAYMENT".equals(order.getStatus())) {
orderModel.add(
linkTo(methodOn(OrderController.class).payOrder(order.getId())).withRel("payment"),
linkTo(methodOn(OrderController.class).cancelOrder(order.getId())).withRel("cancellation")
);
} else if ("PAID".equals(order.getStatus())) {
orderModel.add(
linkTo(methodOn(OrderController.class).getDeliveryStatus(order.getId())).withRel("delivery")
);
}
return orderModel;
}
}
package com.example.controller;
import com.example.dto.OrderDto;
import com.example.dto.OrderModelAssembler;
import com.example.entity.Order;
import com.example.service.OrderService;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
private final OrderModelAssembler assembler;
public OrderController(OrderService orderService, OrderModelAssembler assembler) {
this.orderService = orderService;
this.assembler = assembler;
}
@GetMapping(value = "/{id}", produces = "application/hal+json")
public ResponseEntity<EntityModel<OrderDto>> getOrderById(@PathVariable Long id) {
Order order = orderService.findById(id);
return ResponseEntity.ok(assembler.toModel(order));
}
@PostMapping("/{id}/payments")
public ResponseEntity<Void> payOrder(@PathVariable Long id) {
orderService.processPayment(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id}/cancellations")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancel(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/delivery")
public ResponseEntity<String> getDeliveryStatus(@PathVariable Long id) {
return ResponseEntity.ok("IN_TRANSIT");
}
}
Кешування HATEOAS та заголовок Vary
Якщо сервер динамічно генерує посилання залежно від прав авторизованого користувача (адміністратор бачить посилання rel="delete", а звичайний користувач — ні), відповідь не можна кешувати публічно без зазначення заголовка:
Cache-Control: private, max-age=300
Vary: Authorization
Інакше проміжний кеш (CDN / NGINX) віддасть відповідь із правами адміністратора звичайному користувачу.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
HATEOAS (Level 3 моделі зрілості Річардсона) — це принцип REST, за якого сервер динамічно повертає клієнту посилання на доступні операції разом із тілом ресурсу.
Це дозволяє клієнту орієнтуватися в API без жорстко зашитих URL (decoupling) та реалізує скінченний автомат (State Machine): перелік доступних дій змінюється залежно від стану сутності та прав доступу користувача. Стандартом де-факто для розмітки посилань є формат HAL (
_links).На практиці HATEOAS застосовується рідко через роздування мережевого трафіку (у 2–3 рази для списків), навантаження на CPU і те, що фронтенд-командам значно зручніше працювати зі статичними контрактами OpenAPI.
4 каверзних питання з відповідями
-
Що таке Affordance у контексті HATEOAS? Відповідь: Це гіпермедіа-підказка сервера про дію, яку клієнт може виконати просто зараз із поточного стану ресурсу. Якщо посилання з певним
relвідсутнє в блоці_links, це означає, що операція наразі недоступна для ресурсу або заборонена для прав поточного користувача. -
Яка поширена проблема виникає з посиланнями HATEOAS під час деплою за NGINX або Kubernetes Ingress і як вона вирішується? Відповідь: Проблема спотворення URL: Spring HATEOAS бачить внутрішній IP пода (
10.244.x.x:8080) і генерує посилання на внутрішню адресу за протоколомhttp. Вирішується налаштуванням проксіювання заголовківX-Forwarded-*та увімкненням у Spring Boot налаштуванняserver.forward-headers-strategy=FRAMEWORK(ForwardedHeaderFilter). -
Чому HATEOAS вважається неефективним для високонавантажених систем (Highload)? Відповідь: По-перше, посилання роздувають розмір мережевої відповіді на 30–200%, перевантажуючи смугу пропускання. По-друге, побудова графа посилань на кожному вузлі через рефлексію створює додатковий оверхед на CPU та Garbage Collector під час формування відповідей для великих колекцій.
-
Як коректно кешувати HATEOAS відповіді, якщо набір посилань залежить від ролі користувача? Відповідь: Відповіді повинні позначатися директивою
Cache-Control: private(заборона кешування на публічних CDN/проксі) та обов’язково містити заголовокVary: Authorization, щоб кеш браузера не використовував посилання одного облікового запису для іншого.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «API не можна називати REST, якщо в ньому немає HATEOAS» (в індустрії Level 2 моделі Річардсона вважається повноцінним і достатнім REST API).
- ❌ «HATEOAS прискорює роботу застосунку та економить мережевий трафік» (навпаки, суттєво збільшує розмір payload і навантаження на CPU).
- ❌ «HATEOAS потрібен у всіх мобільних додатках» (у мобільній розробці він практично не використовується через жорстку верстку UI під конкретні сценарії).
- ❌ «Посилання в HATEOAS формуються вручну конкатенацією рядків у контролері» (у Spring використовується
WebMvcLinkBuilderтаRepresentationModelAssembler).