Слово «REST» все понимают по-разному. Для одного это «JSON по HTTP», для другого — строгий набор правил про ресурсы и методы, а для третьего — ответы, которые сами подсказывают клиенту, что делать дальше. Чтобы не спорить, есть удобная лесенка — модель зрелости Ричардсона: она раскладывает «RESTful-ность» на четыре ступени. Верхняя ступень называется HATEOAS. Разберём, что это, зачем и почему до неё почти никто не доходит — и это нормально.
Вся идея и вся её цена помещаются в один пример: один и тот же заказ до возврата и после — и два клиента, которые читают ответ по-разному.
Ссылка refund исчезла из ответа сама — клиент, который рисует кнопки по _links, ничего не правил, а клиент с зашитыми адресами показал кнопку и получил 409. Если таких клиентов большинство, сервер считает ссылки зря.
Модель зрелости Ричардсона
Леонард Ричардсон предложил смотреть на API как на четыре уровня зрелости. Каждый следующий добавляет одну идею.
Лесенка зрелости целиком: снизу одна ручка на всё, выше ресурсы, затем честные методы и коды, и только на вершине ссылки в ответе.
Узнать свой уровень просто. Если в API один адрес вроде /api, куда шлют всё подряд, обычно POST с телом «действие: получить заказ, id: 42», это уровень 0: HTTP здесь просто труба, классический RPC поверх HTTP. Если адреса есть, /orders/42, /customers/7, но отменяют заказ через GET /orders/42/cancel, а ошибка приходит с кодом 200 и текстом внутри, это уровень 1: ресурсы появились, методы используются как попало. Если GET читает, POST создаёт, PUT и PATCH меняют, DELETE удаляет, а ответы приходят с честными 200, 201, 404 и 409, это уровень 2, на нём живёт большинство API, которые называют «REST», и подробно он разобран в статье про REST API. А если ответ содержит не только данные, но и ссылки на действия, которые сейчас доступны, и клиент идёт по ним, как человек кликает по страницам сайта, а не составляет URL сам, это уровень 3, HATEOAS.
Важно: это не оценка «хорошо/плохо». Это описание, сколько идей REST вы применили. Уровень 2 — совершенно рабочая и уважаемая точка.
Что такое HATEOAS
HATEOAS — это аббревиатура от «Hypermedia As The Engine Of Application State»: гипермедиа как движок состояния приложения. Звучит громоздко, а идея простая.
Представьте сайт в браузере. Вы не запоминаете URL каждой страницы — вы видите ссылки и кнопки и кликаете по тем, что доступны сейчас. Если заказ ещё не оплачен, есть кнопка «Оплатить»; после оплаты она пропадает, зато появляется «Вернуть». Браузер не знает заранее структуру сайта — он просто идёт по ссылкам, которые ему прислал сервер.
HATEOAS предлагает то же самое, но для программы-клиента. Сервер вместе с данными присылает список доступных следующих действий в виде ссылок. Клиент не хардкодит адреса вроде /orders/42/cancel — он берёт нужную ссылку из ответа. Если действие сейчас недоступно (заказ уже отменён), ссылки просто не будет, и клиенту не нужно самому знать правила «когда можно отменять».
Пример ответа с гиперссылками
Чаще всего ссылки кладут в отдельный блок. Один из популярных форматов — HAL (Hypertext Application Language): ссылки лежат под ключом _links, а сам ответ отдают с типом application/hal+json — чтобы клиент по заголовку понял, по какому соглашению читать тело:
{
"id": 42,
"status": "PAID",
"amount": 1500,
"_links": {
"self": { "href": "/orders/42" },
"refund": { "href": "/orders/42/refund" },
"invoice":{ "href": "/orders/42/invoice" }
}
}
Что тут происходит. Кроме самих данных заказа сервер сказал: «вот ссылка на меня самого (self), вот действие "вернуть деньги" (refund) и вот "посмотреть счёт" (invoice)». Клиенту не надо знать, как собирается URL возврата, — он берёт готовый href.
А теперь тот же заказ, но уже возвращённый:
{
"id": 42,
"status": "REFUNDED",
"amount": 1500,
"_links": {
"self": { "href": "/orders/42" },
"invoice": { "href": "/orders/42/invoice" }
}
}
Ссылки refund больше нет — возвращать нечего. Клиент, который «просто рисует кнопки по ссылкам», автоматически спрячет кнопку возврата. Правило «когда можно вернуть деньги» осталось на сервере, клиенту его дублировать не пришлось.
Зачем это нужно
У HATEOAS есть два честных плюса.
Слабая связанность клиента и сервера. Клиент не зашивает у себя карту URL и правила «когда какое действие доступно». Сервер может поменять адрес возврата с /orders/42/refund на что-то другое — клиент не сломается, потому что берёт ссылку из ответа, а не собирает её сам. Логика «что доступно» живёт в одном месте — на сервере.
Самодокументируемость. Ответ сам показывает, что с ресурсом можно делать дальше. В идеале клиент способен «исследовать» API, переходя по ссылкам от корня, почти как человек ходит по сайту, — не заглядывая постоянно в отдельную документацию.
Почему на практике доходят редко
Теперь честно про минусы — из-за них уровень 3 остаётся скорее теорией.
Сложнее серверу. Каждый ответ надо обвешивать ссылками, считать, какие действия сейчас доступны, поддерживать формат вроде HAL. Это заметно больше кода, чем просто отдать данные.
Сложнее клиенту — если он вообще хочет это использовать. Красивая идея «клиент ходит по ссылкам и ничего не хардкодит» требует умного клиента, который умеет находить действия по их именам и реагировать на их появление/исчезновение. На деле почти все клиенты всё равно знают структуру API заранее и просто игнорируют блок _links — тогда вся дополнительная работа сервера пропадает впустую.
Мало инструментов и привычки. Вокруг уровня 2 выстроена вся экосистема: генераторы клиентов, документация OpenAPI, тестовые инструменты. Полноценный HATEOAS-клиент, который динамически ходит по ссылкам, поддерживают единицы, и у команд нет устоявшихся практик.
Итог трезвый: большинство API, которые называют «REST», на самом деле остаются на уровне 2 — ресурсы плюс правильные HTTP-методы. И это совершенно нормально. HATEOAS полезно знать как идею и как верхнюю планку модели, но тащить его в каждый проект не нужно.
Где это применяется
Отдельные элементы HATEOAS встречаются чаще, чем полный уровень 3. Обычно берут не всё, а самое полезное:
- Ссылки на связанные ресурсы. Отдать вместе с заказом ссылку на клиента и на счёт — дёшево и удобно, даже если остальное осталось на уровне 2.
- Пагинация. Классический удачный случай: сервер присылает ссылки
nextиprev, и клиенту не надо самому клеить query-параметры для следующей страницы. - Процессы с состояниями. Там, где у сущности есть жизненный цикл (черновик → оплачен → отправлен → возвращён), ссылки на доступные переходы избавляют клиента от дублирования правил.
Где спотыкаются начинающие
- Путают «REST» с уровнем 3. Услышав, что «настоящий REST — это HATEOAS», начинают считать свой рабочий API «ненастоящим». Уровень 2 — законная и самая распространённая цель, не комплексуйте.
- Строят HATEOAS, которым никто не пользуется. Обвешивают ответы ссылками, а клиент всё равно хардкодит URL и игнорирует
_links. Получается работа сервера впустую — сначала убедитесь, что клиент реально будет ходить по ссылкам. - Изобретают свой формат ссылок в каждом endpoint. Если уж делать, берите готовое соглашение (HAL) единообразно, а не «тут
_links, тамactions, а рядом простоurl». - Тащат полный уровень 3 в простой CRUD. Для пары справочников гиперссылки на действия — лишняя сложность без отдачи. Начинайте с чистого уровня 2.
Что учить рядом
HATEOAS — это верхушка модели зрелости, поэтому фундамент под ней важнее всего: сначала уверенно освойте REST API на уровне ресурсов и HTTP-методов. Рядом стоят другие стили контракта — gRPC для быстрых внутренних вызовов и GraphQL для гибких срезов данных; полезно понимать, чем они отличаются от REST. Транспорт под всем этим — протокол HTTP, его методы и коды статусов как раз и составляют уровень 2. А как выбор стиля контракта вписывается в проектирование системы целиком — в разделе системного дизайна.
Промежуточный вариант, который берут чаще всего
Полный уровень зрелости с гиперссылками встречается редко, а нужда «клиент не должен знать бизнес-правила» — постоянно. Поэтому в жизни чаще всего берут упрощённый вариант: список доступных действий вместо ссылок.
{
"id": "ord-42",
"status": "PAID",
"availableActions": ["refund", "cancel"]
}
Или флаги: canRefund: true, canCancel: false. Разница с полным вариантом одна — клиент всё ещё собирает адрес сам, то есть связанность по адресам остаётся. Зато главная выгода сохраняется: правила живут на сервере, и кнопка на экране появляется по ответу, а не по логике, продублированной в клиенте.
Что выбрать. Флаги проще всего и годятся, когда действий два-три и они фиксированы. Список действий лучше, когда их больше и набор может расшириться (клиент, не знающий нового действия, просто его не покажет). Полные ссылки нужны, когда клиентов много и они внешние — то есть когда вы действительно хотите менять адреса, не согласовывая это ни с кем.
Чем расплачивается
Причины, по которым до третьего уровня доходят редко, серьёзнее, чем «мало инструментов».
Ответ становится персональным. Набор ссылок зависит от прав и состояния — значит, два клиента получают разные ответы на один и тот же запрос. Отсюда прямое следствие: общий кэш (прокси, сеть доставки) становится бесполезным, а Cache-Control обязан быть private. Для публичного каталога, который кэшируется на краю сети, это может быть решающим доводом против.
Ответ растёт. Десять ссылок с адресами и именами — это лишние полкилобайта на объект. В списке из пятидесяти элементов это уже заметно, и приходится решать, отдавать ли ссылки в элементах списка или только в карточке.
Генератор клиента по ссылкам не ходит. Это главная практическая причина. Инструменты, которые строят клиента по описанию API, генерируют вызовы по путям: метод, путь, параметры. Модель «получи ответ, найди связь, перейди по ней» в такую генерацию не укладывается — значит, клиента придётся писать руками, и вся выгода автоматизации теряется. Описать ссылки в OpenAPI можно (раздел links), но генераторы этим почти не пользуются.
Тестировать сложнее. Контрактный тест «ручка возвращает такой-то объект» превращается в «по этому состоянию должны быть такие связи и не должно быть таких» — правил больше, и они завязаны на состояние.
Отсюда честный вывод: полные гиперссылки оправданы там, где клиентов много, они внешние и вы не можете их выкатывать; в остальных случаях берут доступные действия или флаги — дешевле, а главная выгода (правила на сервере) остаётся.
Глубже: rel: имя связи важнее адресарасширенное
Вся идея слабой связанности держится на одной вещи, которую легко пропустить: клиент опирается на имя связи (rel), а не на адрес.
{
"id": "ord-42",
"status": "PAID",
"_links": {
"self": { "href": "/api/v1/orders/ord-42" },
"refund": { "href": "/api/v1/orders/ord-42/refunds" },
"cancel": { "href": "/api/v1/orders/ord-42/cancel" }
}
}
Клиент не собирает путь сам («возьму идентификатор и допишу /refund») — он ищет в ответе связь с именем refund и идёт по её адресу. Значит, сервер может изменить адрес, перенести действие в другой сервис, добавить в путь параметр — и клиент продолжит работать, потому что имя связи не изменилось.
Отсюда и второе свойство, ради которого всё затевалось: отсутствие связи — это тоже информация. Нет связи refund — возврат сейчас невозможен (не тот статус, нет прав, истёк срок). Клиенту не нужно дублировать бизнес-правила, чтобы решить, показывать ли кнопку: он показывает то, что сервер разрешил.
Имена связей берут из реестра стандартных (self, next, prev, first, last, collection) или объявляют свои — тогда их описывают в документации и не переименовывают: имя связи это часть контракта, ровно как имя поля.
Глубже: как это собирают в Javaрасширенное
Для ориентира: в Spring есть отдельный модуль поддержки гиперссылок (Spring HATEOAS). Он даёт обёртки над моделью (EntityModel, CollectionModel), сборку ссылок из методов контроллера (linkTo(methodOn(...)), чтобы адрес не писать строкой) и готовый формат представления (application/hal+json). Второе особенно важно: ссылки собираются из тех же методов, что обслуживают запросы, — значит, переименование пути не рассыпает ссылки молча.
Коротко
- Модель зрелости описывает три шага: ресурсы вместо одной ручки, правильные методы, гиперссылки в ответе; на практике останавливаются на втором.
- Смысл гиперссылок — в именах связей: клиент ищет связь
refund, а не собирает путь сам, поэтому адрес можно менять. Отсутствие связи означает «сейчас нельзя» — правило остаётся на сервере. - Чаще полного варианта берут упрощённый: список доступных действий или флаги
canRefund. Правила по-прежнему на сервере, связанность по адресам остаётся. - Платят кэшированием (ответ персональный, только
private), объёмом ответа и тем, что генератор клиента по ссылкам не ходит — клиента придётся писать руками. - Имя связи — часть контракта: его не переименовывают, а свои имена описывают в документации.
Что почитать дальше
- URL и ресурсы — второй уровень зрелости, на котором останавливаются: ресурсы и методы.
- Действия и псевдонимы — как оформляют команды, которые не ложатся на ресурсы.
- Версионирование API — что делать, когда адрес всё-таки меняется.
- API-first и контракт — почему генерация клиента важнее гиперссылок в большинстве проектов.