Практические советы по созданию REST API
Сайтостроение | создано: 16.05.2024 | опубликовано: 17.05.2024 | обновлено: 16.08.2024 | просмотров: 517 | всего комментариев: 5
Самые полезные советы по созданию REST API сервисов. Несколько советов, которые были проверены временем на реальных проектах.
Лучшие практики REST
С опытом приходит понимание, что всё меняется. В том числе и незыблемые правила и паттерны, которые на тот момент времени казались "железобетонными". В этой статье хочу поделиться опытом, который был мной приобретен за долгие годы практики в разработки API разных форматов, с разными требованиями, на разных платформах, для разных заказчиков, с разным уровнем поддержки и т.д. и т.п. Опыт трансформирую в набор правил, которые подтверждены суровым "прокурором" под названием "время". Итак.
Правило 1: Всё хорошо в меру
Многие разработчики понимают «REST» как своего рода API на базе HTTP с URL-адресами, в основе которых лежат имена существительные. Это было заложено в REST при его задумке. Но всё меняется, и поэтому не нужно стремиться поддерживать устаревшие правила именования, создавайте полезные и прагматичные API с понятными маршрутами. Но всё хорошо в меру.
Правило 2: Множественная форма
Используйте множественное число для сущностей в маршрутах:
# ПРАВИЛЬНО
GET /products # вернёт все продукты
GET /products/{product_id} # вернёт один продукт с определенным идентификатором
# НЕПРАВИЛЬНО
GET /product/{product_id}
Правило 3: Не используйте ненужные сегменты
Не используйте ненужные сегменты маршрута (которые изначально были определены в спецификации REST):
# ПРАВИЛЬНО
GET /v3/api/listings/{listing_id}
# НЕПРАВИЛЬНО
PATCH /v3/api/shops/{shop_id}/listings/{listing_id}
GET /v3/api/shops/{shop_id}/listings/{listing_id}/properties
PUT /v3/api/shops/{shop_id}/listings/{listing_id}/properties/{property_id}
Правило 4: Не используйте расширение в URL
Добавлять подобную идентификацию формата было заложено изначально в REST при его создании. Тогда, в начале века еще были вопросы относительно того, что нужно потребителю API - JSON или XML. На данный момент вопрос решен всегда возращайте JSON и не используйте в URL расширения типа *.json (или другие) для определения формата отдачи. А если клиенты захотят договориться о чем-то другом, полагайтесь на стандартные заголовки HTTP (Accept, Accept-Charset, Accept-Encoding, Accept-Language).
Правило 5: Не возвращайте массивы как верхний уровень в ответе
Всегда добавляйте название типа для массива.
# ПРАВИЛЬНО
GET /items возвращает:
{ "data": [{ ...item1...}, { ...item2...}] }
# НЕПРАВИЛЬНО
GET /items возвращает:
[{ ...item1...}, { ...item2...}]
Проблема в том, что при возврате массивов очень сложно внести обратно-совместимые изменения. Если возвращать объекты, то они позволяют вносить дополнительные изменения. Напимер, вам может потребоваться количество записей totalItems, или другие данные сопровождающие или дополняющие первоначальный массив данных. При правильном подходе, вам потребуется всего лишь добавите новые свойства, и больших переделок не потребуется. При неправильном подходе потребуется менять версию API или другой глобальный рефакторинг.
Правило 6: Не возвращайте map-структуры
Довольно часто я наблюдаю в результатах запросов возвращаются map-структуры. Вместо этого верните массив объектов.
# ПРАВИЛЬНО (коррелирует с правилом №5)
GET /items возвращает:
{
"data": [
{ "id": "KEY1", "foo": "bar" },
{ "id": "KEY2", "foo": "baz" },
{ "id": "KEY3", "foo": "bat" }
]
}
# НЕПРАВИЛЬНО
GET /items возвращает:
{
"KEY1": { "id": "KEY1", "foo": "bar" },
"KEY2": { "id": "KEY2", "foo": "baz" },
"KEY3": { "id": "KEY3", "foo": "bat" }
}
В JSON объекты типа map-структуры:
- Ключевая информация избыточна и добавляет шум в провод.
- Ненужные динамические ключи создают головную боль для людей, работающих с типизированными языками.
- Что бы вы ни думали о «естественном» ключе, он может измениться, или клиентам может потребоваться другая группировка.
Но есть исключение для map-стуктур, когда они приветствуются - это когда вы возвращаете пары "ключ/значение".
# ПРАВИЛЬНО
{
"key1": "value1",
"key2": "value2"
}
Правило 7: Возвращайте идентификаторы как строки
Всегда используйте строки для идентификаторов объектов, даже если ваше внутреннее представление (т. е. тип столбца базы данных) является числовым. Просто сформулируйте число.
# ПРАВИЛЬНО
{ "id": "123" }
# НЕПРАВИЛЬНО
{ "id": 123 }
Хороший API будет работать долго и, возможно, переживет вас. За это время ваша инфраструктура может быть переписана на другой технологической платформе, перенесена в новую базу данных или объединена с другой базой данных, содержащей конфликтующие идентификаторы. Строковые идентификаторы невероятно гибки. Строки могут кодировать информацию о версии или диапазоны идентификаторов сегментов. Строки могут кодировать составные ключи. Числовые идентификаторы накладывают связывают руки разработчиков, которые будут работать с вашим API в будующем.
В качестве бонуса, если все ваши поля идентификаторов являются строками, разработчикам клиентов, работающим на типизированных языках, не нужно думать о том, какой тип использовать. Просто используйте строки!
Правило 8: Не возвращайте 404
В спецификации HTTP указано, что вам следует использовать код 404, чтобы указать, что ресурс не найден. Это говорит о том, что вы должны возвращать 404 для запросов GET/PUT/DELETE/etc к идентификатору, который не существует. Таким образом, при запросе GET /items/{item_id} для объекта, которого не существует, ответ должен указывать, что:
- сервер понял ваш запрос и
- объект не был найден.
К сожалению, ответ 404 не гарантирует это. Ответ со статусом 404 может быть возвращен в ряде случаев, которые вы не можете контролировать:
- Неправильно настроенный клиент обращается к неправильному URL-адресу
- Неправильно настроенные прокси (клиентская и серверная части)
- Неправильно настроенные балансировщики нагрузки
- Неправильно настроенные таблицы маршрутизации в серверном приложении.
Другими словами, возврат 404 для «объект не найден» очень похож на возврат HTTP 500. Ошибка может означать, что вещь не существует, или это может означать, что что-то пошло не так. Следовательно, клиент не может быть уверен, что именно не получилось.
Ясное дело, что это правило не такое однозначное, как предыдущие и я могу снова применить тезис "всё хорошо в меру". Можно применять пару подходов для реализации однозначности полученного от API ответа.
- Использовать код 410 ("был но теперь нет"), которые более точно описывает обозначенные вначале требования к несуществующему объекта: "сервер понял ваш запрос и этот объект не найден"
- Всё-таки использовать 404 при возврате ответа на запрос, но при этом использовать собственный объект (тело) ошибки с описанием того, о чём именно говорит статус 404.
- Считать 404 ошибку как составную часть бизнес-логики, то есть оборачивать ошибку в спецификацию RFC 9457 (устаревший RFC 7807), то есть выдавать объект (из предыдущего пункта) с расширенный информацией).
Любая другая стратегия лучше, чем просто возвращать 404.
Правило 9: Будьте последовательными
Это важно правило, но оно скорее облегчит работы не вам, а вашим последователям (про прощения за каламбур). Те кто будет смотреть ваш код в будущем или использовать ваш API не должны завадать вопросы типа этих:
- Почему в сущности person есть свойство order_id
- Зачем в коллекции адресов филиалов компании есть координаты складов
Правило 10: Реализуйте идемпотентные механизмы
Реализуйте по возможности идемпотентные механизмы там, где их можно реализовать. Поскольку сеть ненадежна, в случае возникновения ошибки клиент не сможет узнать, успешно ли завершилась операция на сервере. Например, если клиент снова отправит заказ, мы можем создать дубликаты заказов («at-least-once»). Если клиент не отправит заказ повторно, мы можем потерять заказы («at-most-once»).
Чтобы получить однократное поведение для неидемпотентных операций, потребуется дополнительные управляющие механизмы между клиентом и сервером. Обычно используют два способа поддерживать подобные механизмы.
Способ А: «Ключ идемпотентности» или «идентификатор ссылки на клиент»
POST /v1/customers
Idemptency-Key: <unique-request-id>
{"name":"Bob Dobbs"}
Таким образом, многие системы обработки заказов позволяют клиентам отправлять «идентификатор клиента», который сохраняется в каждом заказе и включается в отчеты клиентов. Обеспечение уникальности этого значения защищает от бесконечного дублирования заказов.
Способ Б: Позволить клиенту создавать идентификаторы
Если клиенту необходимо выбрать уникальный ключ идемпотентности для каждой отправки, почему бы просто не сделать его идентификатором?
# Client picks the id
POST /items
{"id": "item1"}
# The id can now be used
GET /items/item1
Это может привести к созданию простых API, хотя это усложняет реализацию в мультитенантных системах, где идентификатор должен быть уникальным для каждого арендатора.
Правило 11: Используйте ISO8601 для даты
Используйте строки для дат, а не числа, такие как миллисекунды с эпохи. Читабельность - имеет значение! Кто-то, взглянув на «2023-12-21T11:17:12.34Z», может заметить, что это месяц в будущем, а кто-то даже заглядывать на 1703157432340 не будет.
ISO8601 стандартизирует форматы для многих других концепций, связанных с датой и временем, включая локальные (без зоны) даты и время, продолжительность и интервалы. Используйте их.
Многие платформы разработки по умолчанию не генерируют форматы ISO8601. Хуже того, формат по умолчанию часто меняется в зависимости от локали и/или часового пояса компьютера! Каждый раз, когда вы форматируете значение даты и времени в своем API, проверяйте выходные данные. Всегда есть способ сгенерировать ISO8601.
Заключение
Данная статья включает в себя правила не только опробованные мной лично на разных пректах, но и найденные в интернете правила, с которыми я согласен. Вы можете прислать свои правила в комментариях к статье. Обсудим, добавим в список, или не добавим.
Комментарии к статье (5)
Сергей, можете 3-ье правило более подробно объяснить?
Денис,
Честно говоря, не знаю как объяснить ещё подробнее. Суть в том, что можно маршрут придумать какой угодно, а правильно было бы использовать как понятнее в контексте вашей предметной области. REST хорош, но и его спецификация имеет место устаревать. Всё хорошо в меру.
Если я правильно понял третий пункт, то не стоит делать глубоких вложенностей для навигации. Можно сокращать путь.
Денис,
Да, так и есть. Каждый сегмент - вложение. Но ининогда они нужны. Главное, не делать вложенность без необходимости.
Вы забыли ещё добавить к ответу: всё хорошо в меру:)