Что такое REST API и почему ваш — вероятно, не REST

Большинство разработчиков строят не REST API, а JSON-over-HTTP. Рой Филдинг определил 6 ограничений — разбираем каждое и выясняем, почему HATEOAS почти везде пропускают.

Обложка: Что такое REST API и почему ваш — вероятно, не REST

Большинство разработчиков хотя бы раз строили «REST API». Мало кто читал диссертацию, которая его определяет. Этот разрыв между популярным пониманием и оригинальной спецификацией порождает повторяющиеся архитектурные проблемы и нестабильность API.

REST API — это веб-сервис, удовлетворяющий шести архитектурным ограничениям, выведенным Роем Филдингом в докторской диссертации «Architectural Styles and the Design of Network-based Software Architectures» (UC Irvine, 2000 год). Начав с «нулевого стиля» — пустого набора без ограничений — Филдинг добавлял каждое из них последовательно, анализируя порождаемые ими свойства распределённых гипермедиа-систем (систем, где переходы между состояниями описаны прямо в ответах сервера). Результат получил название «Передача репрезентативного состояния» (Representational State Transfer).

Индустрия взяла название и проигнорировала большинство ограничений. «REST» теперь означает «любой API, который отправляет JSON по HTTP». Если вы строите публичный API или API для команд за пределами вашей организации — пропущенные ограничения начинают стоить денег.

Ключевые выводы

REST — это шесть архитектурных ограничений, сформулированных Роем Филдингом в 2000 году, а не «любой JSON-over-HTTP API».

Большинство API выполняют лишь 2–3 ограничения из шести: клиент-сервер, stateless и частично слоистую систему.

Самое игнорируемое ограничение — HATEOAS: сервер передаёт клиенту список доступных действий прямо в ответе.

HATEOAS решает три дорогостоящие проблемы: пагинацию, версионирование API и обнаруживаемость ресурсов.

Для небольшой команды с одним потребителем пропуск HATEOAS оправдан. Для публичного API — нет.

Шесть ограничений REST API: разбор по порядку

Ограничение 1: Клиент-сервер

Клиент и сервер имеют разные зоны ответственности. Клиент отвечает за интерфейс, сервер — за данные и логику. Большинство API справляются с этим по умолчанию. Нарушение появляется, когда сервер начинает диктовать, как клиент должен отображать информацию.

Например, если API возвращает displayOrder: 3 и buttonColor: "#ff0000" для какого-либо действия — это нарушение. Порядок отображения должен следовать из позиции элементов в ответе. Цвет должен определяться семантическим свойством вроде class: ["danger"], которое каждый клиент интерпретирует самостоятельно.

Ограничение 2: Stateless (без состояния)

Каждый запрос содержит всю информацию, необходимую серверу для его обработки. Сервер не хранит состояние сессии между вызовами.

Если вы отправляете GET /path-1 с сессионной cookie, и сервер ищет её в памяти, чтобы получить ваш ID пользователя — это серверное состояние. Stateless-версия включает ID прямо в запрос: JWT или тело POST-запроса переносят его вместе с запросом и могут вернуть в ответе для повторного использования клиентом.

Ограничение 3: Кэшируемость

Ответы должны быть явно или неявно помечены как кэшируемые или некэшируемые. Клиент или промежуточный узел может повторно использовать закэшированные ответы, не обращаясь к серверу. Филдинг рассматривал кэшируемость как архитектурную задачу первого класса, повышающую эффективность и воспринимаемую производительность за счёт снижения средней задержки.

Большинство JSON API полностью игнорируют кэширование. Вы отправляете GET /articles/42, а в ответе нет ни Cache-Control, ни ETag, ни Last-Modified. Клиент обращается к серверу каждый раз, даже если статья не менялась неделями.

Ограничение 4: Единый интерфейс (Uniform Interface)

Это главное ограничение. Филдинг разбил его на четыре подограничения — три описываются ниже, четвёртое (HATEOAS) вынесено в отдельный раздел из-за его значимости.

4.1 URI идентифицируют ресурсы. Единообразие здесь — это сама спецификация URI: схема, authority, путь, запрос, фрагмент. Ограничение ничего не говорит о структуре сегмента пути: /articles/42, /x?id=42 и /a/b/c — всё это валидные URI. «Используйте чистые URL-пути» — популярное соглашение и хороший SEO-инструмент, но не то, что требует Филдинг.

4.2 Управление ресурсами через представления. Вы выполняете GET в /whatever, чтобы получить представление ресурса. Заголовок Content-Type сообщает серверу формат тела запроса. Заголовок Accept сообщает, какие медиатипы (форматы обмена данными) поддерживает клиент для ответа.

4.3 Самоописывающие сообщения. Ответ с Content-Type: application/vnd.collection+json сообщает клиенту, как разбирать тело, без каких-либо предположений.

Ограничение 4.4: HATEOAS — то, что пропускают почти все

HATEOAS (Hypermedia As The Engine Of Application State) — четвёртое подограничение Uniform Interface. С первыми тремя большинство API справляются. HATEOAS — место, где останавливается почти каждый.

Разница хорошо видна на примере API для списка чтения. Без HATEOAS вы получаете просто данные, похожие на запись в базе:

			{
  "id": 42,
  "title": "How Browsers Work",
  "url": "https://example.com/browsers",
  "status": "unread"
}
		

Клиент ничего не знает о том, что он может сделать дальше. Чтобы отметить статью как прочитанную, клиент уже должен знать endpoint: PATCH /articles/42 с {"status": "read"}. Разработчик захардкодил эти знания, прочитав документацию. Сам API их не сообщил.

С HATEOAS сервер сообщает клиенту о доступных действиях в стандартизированном виде. Вот тот же ответ с использованием Siren — одного из стандартизированных медиатипов для гипермедиа-API:

			{
  "class": ["article"],
  "properties": {
    "id": 42,
    "title": "How Browsers Work",
    "url": "https://example.com/browsers",
    "status": "unread"
  },
  "actions": [
    {
      "name": "mark-as-read",
      "href": "/articles/42",
      "method": "PATCH",
      "fields": [
        { "name": "status", "value": "read" }
      ]
    },
    {
      "name": "delete",
      "href": "/articles/42",
      "method": "DELETE"
    }
  ],
  "links": [
    { "rel": ["self"], "href": "/articles/42" },
    { "rel": ["collection"], "href": "/articles" },
    { "rel": ["next"], "href": "/articles/43" }
  ]
}
		

Клиент не хардкодит URL и HTTP-методы. Массив actions сообщает, что можно сделать. Навигация приходит из links. Если статья уже прочитана — сервер исключает действие mark-as-read из ответа. Кнопка исчезает в UI. Без единого условного выражения в клиентском коде.

Сервер добавляет новое действие — и каждый клиент подхватывает его при следующем запросе, без деплоя. Это принципиальное отличие: сервер управляет доступными переходами состояния.

Ограничение 5: Слоистая система

Клиент не может определить, общается ли он с конечным сервером или с промежуточным узлом. Балансировщики нагрузки, CDN и API-шлюзы должны быть прозрачны для вызывающей стороны. Большинство API выполняют это ограничение автоматически. Самый распространённый вид нарушения — когда сообщения об ошибках раскрывают имя хоста бэкенд-сервиса, тем самым нарушая прозрачность слоёв.

Ограничение 6: Код по требованию (необязательное)

Сервер может передавать исполняемый код клиенту. Думайте о JavaScript, подключаемом через тег <script> на веб-странице. Для API это ограничение почти не применимо. Филдинг сделал его единственным необязательным.

Что HATEOAS решает на практике

Филдинг разработал HATEOAS для решения тех самых проблем, с которыми API-команды сейчас борются вручную, многократно и каждый раз по-разному.

Пагинация

Без HATEOAS каждый API изобретает собственную схему. Один использует page и pageSize. Другой — offset и limit. Третий — токены на основе курсора. С HATEOAS сервер включает ссылку с отношением «следующая страница». Клиент следует этому отношению. Схема пагинации может измениться, не сломав ни одного клиента: клиент не знал деталей схемы и не знает формата URL.

Версионирование

Без HATEOAS команды версионируют API через URL-пути (/v1/, /v2/) или кастомные заголовки вроде X-API-Version. Поддержка нескольких версий занимает месяцы, клиенты привязываются к версии и ломаются при её выводе из эксплуатации. С HATEOAS сервер вводит новые действия, добавляя ссылки. Старые ссылки продолжают работать.

Обнаруживаемость

Без HATEOAS первый шаг разработчика — чтение Swagger-документации, второй — хардкодинг каждого endpoint в клиент. С HATEOAS корень API возвращает ссылки на все доступные ресурсы. Клиент исследует API так же, как браузер исследует веб-сайт.

Сколько ограничений выполняет ваш API

Итого шесть ограничений. Большинство API удовлетворяют двум-трём: клиент-сервер, слоистую систему и частичную безгосударственность. Большинство нарушают кэшируемость по умолчанию. Большинство полностью игнорируют HATEOAS.

  • Клиент-сервер — разделение ответственности за интерфейс и данные
  • Stateless — каждый запрос самодостаточен, без серверных сессий
  • Кэшируемость — явные заголовки Cache-Control, ETag, Last-Modified
  • Единый интерфейс — URI, представления, самоописывающие сообщения и HATEOAS
  • Слоистая система — прозрачность промежуточных узлов
  • Код по требованию — опционально, для API почти не применимо

Это не оценка. Это карта компромиссов, которые вы приняли — намеренно или случайно. Для небольшой команды с одним потребителем и Slack-каналом для координации пропуск HATEOAS оправдан. Публичный API с сотнями потребителей платит за каждый пропущенный раунд миграции версий. Подробнее об эволюции REST-архитектуры — в главе 5 диссертации Филдинга.

Часто задаваемые вопросы
1
Что такое REST API на самом деле?

REST API — это веб-сервис, удовлетворяющий шести архитектурным ограничениям Роя Филдинга (2000 год): клиент-сервер, stateless, кэшируемость, единый интерфейс (включая HATEOAS), слоистая система и опциональный код по требованию. Большинство API, называемых REST, на самом деле являются «HTTP API с JSON» и выполняют лишь 2–3 из шести ограничений.

2
Что такое HATEOAS и зачем он нужен?

HATEOAS (Hypermedia As The Engine Of Application State) — ограничение, при котором сервер включает в каждый ответ список доступных действий и ссылок. Клиент не хардкодит URL и не читает документацию — он следует ссылкам, как браузер следует гиперссылкам. Это решает проблемы версионирования, пагинации и обнаруживаемости без изменений на стороне клиента.

3
Нужно ли реализовывать HATEOAS в каждом API?

Нет. Для небольшого внутреннего API с единственным потребителем затраты не оправданы. Стоимость растёт с количеством потребителей, миграциями версий и количеством ломающих изменений. Публичные API с внешними потребителями получают от HATEOAS наибольшую выгоду.

4
Какие медиатипы поддерживают HATEOAS?

Несколько стандартизированных форматов: Siren (application/vnd.siren+json) описывает действия, поля и ссылки; HAL — Hypertext Application Language (application/hal+json) — предоставляет встроенные ресурсы и ссылки; JSON:API (application/vnd.api+json) определяет структуру отношений. Каждый из них позволяет создавать клиенты, не знающие конкретных endpoint.

5
Почему большинство API не соответствуют стандарту REST?

Преимущественно из-за неосведомлённости и прагматики. Термин REST стал синонимом «JSON API», и большинство разработчиков учились REST именно в этом упрощённом понимании. Реализация HATEOAS требует выбора медиатипа, обучения команды и изменения клиентского кода — инвестиции, которые малые команды не готовы делать.

Выводы

Я разочарован тем, что многие не знакомы с 15-летними исследованиями в области гипермедиа, которые стоят за REST. Большинство так называемых REST API — это просто удалённые вызовы процедур через HTTP.
Рой Филдингавтор REST-архитектуры, 2008 (источник: <a href="https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven">roy.gbiv.com</a>)

Пройдитесь по шести ограничениям и посчитайте, сколько из них выполняет ваш API. Это даст не оценку, а карту принятых компромиссов — осознанных или случайных. Упражнение отвечает на один вопрос: ваш API — это Representational State Transfer или просто HTTP-транспорт, закрытый для расширения?

Оригинальная статья: Fagner Brack — What Is a REST API, and Why Yours Probably Isn't One. Первоисточник: Глава 5 диссертации Роя Филдинга, UC Irvine.