Что такое 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-представлений используется RepresentationModelAssemblerSupport:
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 и сборщик мусора (GC) при формировании ответов для больших коллекций.
-
Как корректно кэшировать HATEOAS ответы, если набор ссылок зависит от роли пользователя? Ответ: Ответы должны помечаться директивой
Cache-Control: private(запрет кэширования на публичных CDN/прокси) и обязательно содержать заголовокVary: Authorization, чтобы кэш браузера не использовал ссылки одной учетной записи для другой.
Красные флаги (чего категорически нельзя говорить)
- ❌ «API нельзя называть REST, если в нем нет HATEOAS» (в индустрии Level 2 модели Ричардсона повсеместно считается полноценным и достаточным REST API).
- ❌ «HATEOAS ускоряет работу приложения и экономит сетевой трафик» (наоборот, существенно увеличивает размер payload и нагрузку на CPU).
- ❌ «HATEOAS нужен во всех мобильных приложениях» (в мобильной разработке он практически не используется из-за жесткой верстки UI под конкретные сценарии).
- ❌ «Ссылки в HATEOAS пишутся вручную конкатенацией строк в контроллере» (в Spring используется
WebMvcLinkBuilderиRepresentationModelAssembler).