Skip to content

akrisanov/http-api-design

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 

Repository files navigation

Руководство по дизайну HTTP API сервисов

Данное руководство описывает дизайн HTTP API сервисов с обменом данными в формате JSON.

Основная цель документа – предоставить консистентность и фокус на бизнес логике, избегая изобретения «велосипедов».

Общие принципы

  • URL идентифицирует ресурс
  • URL включают существительные, а не глаголы
  • Для именования ресурсов (коллекций) используются существительные множественного числа, например, contents вместо content. Однако если мы говорим про метод, который идемпотенто выполняет действие для текущего пользователя, то его название может быть в единственном числе. Например:
    • /profile — возвращает данные авторизованного пользователя и только его
    • /profiles — может возвращать данные пользователей, например, для просмотра правами администратора
    • /basket — корзина пользователя в интернет-магазине
  • Для работы с ресурсами выполняются HTTP методы:
    • GET
    • POST
    • PUT
    • PATCH
    • DELETE
  • Используется фильтрация вместо вложенных ресурсов: /products?category=food&category=health, а не /category/food/products. Вложенные ресурсы навязывают отношения, которые могут измениться, и делают написание клиентов более трудоёмким.
  • Версия API представлена в виде даты, задокументированной в журнале изменений. Номер версии не указывается в URL.

RESTful URLs

Примеры хороших запросов

Список контента:

GET /contents

Фильтрация посредством параметров запроса:

GET /contents?status=draft&sort=-created_at
GET /contents?sort=-created_at

Одна сущность контента:

GET /contents/{id}

Получение нескольких ресурсов:

GET /contents?id=1&id=2&id=3&id=4

Действие над ресурсом:

POST /contents/{id}/actions/reindex

Функциональность выборки полей позволит уменьшить payload ответа и ускорить десериализацию данных на клиентах.

Раскрытие вложенных ресурсов в ответе:

GET /contents/{id}?expand=files

Добавление только выбранных полей в ответ:

GET /contents/{id}?fields=title,sort_index

Примеры плохих запросов

Названия ресурсов в единственном числе:

GET /content
GET /content/{id}
GET /content/action

Глаголы действий в URL:

GET /content/create

Вложенные ресурсы:

GET /categories/{id}/articles

Фильтрация вне query string:

GET /contents/-is_top

Фильтрация для получения нескольких ресурсов:

GET /contents?id[]=1&id[]=2

Использование параметров в запросе

/entity?query_param1=1&query_param2=1&query_param2=2&query_param2=3

Невалидные значения параметров должны вызывать ошибку, а не игнорироваться.

Имя параметра всегда одинаково вне зависимости от множественности значений.

В качестве методов сравнения в параметрах запроса применяются следующие lookup'ы:

Lookup Эквивалент
__gt >
__gte >=
__lt <
__lte <=

Методы HTTP

Как методы HTTP соотносятся с операциями создания, чтения, обновления и удаления ресурсов:

Метод HTTP POST GET PUT PATCH. DELETE
CREATE READ UPDATE UPDATE DELETE
/contents Создание сущности контента Список сущностей контента Пакетное обновление сущностей контента Удаление всех сущностей контента
/contents/1 Сущность контента Полное /частичное обновление сущности контента Обновление сущности контента используя JSON Patch формат, иначе ошибка Удаление сущности контента

Действия

API избегает действий над ресурсами и разделяет ресурсы когда это возможно.

Хорошие запросы:

GET  /users/{id}
GET  /hires?user={id}

POST /hires?user={id}&start_date=13.03.2020

Плохие запросы:

GET  /users/{id}/hire
POST /users/{id}/hire?start_date=13.03.2020

Когда действие над ресурсом действительно необходимо в рамках его контекста, оно реализуется в url_path /actions.

Действия всегда идемпотентны.

Пример:

POST /users/{id}/actions/deactivate

Ответы

В ответах не используются значения в качестве ключей.

Хороший ответ:

"tags": [
  {"key": "management", "title": "Менеджмент"},
  {"key": "python", "title": "Python"}
]

Плохой ответ:

"tags": [
  {"management": "Менеджмент"},
  {"python", "Python"}
]

Коды HTTP статусов

Статус Описание
200 OK Запрос выполнился с ожидаемым результатом
201 Created Успешное создание ресурса
204 No Content Запрос, не требующий payload для клиента, успешное выполнение действия
400 Bad Request Невалидный JSON или др. ожидаемая ошибка (ошибка структуры запроса, параметров запроса)
401 Unauthorized JWT токен не предоставлен / не валиден
403 Forbidden Пользователь не имеет нужных прав доступа для выполнения действия
404 Not Found Запрашиваемый ресурс не существует
405 Method Not Allowed Действие запрещено для данного ресурса в его текущем состоянии (не путать с авторизацией)
409 Conflict Запрашиваемый ресурс уже существует, идемпотентный запрос
413 Request Entity Too Large Недопустимый размер payload
415 Unsupported Media Type Недопустимый тип формата запроса, на основе Content-Type или Content-Encoding заголовков
422 Unprocessable Entity Невалидные значения параметров запроса / запрос не прошел валидацию
429 Too Many Requests Клиент превысил допустимое кол-во запросов к API (rate limit)
500 - 504 Server error Что-то пошло не так не стороне веб или application-сервера API

Обработка ошибок

В общих случаях payload включает в себя сообщение для пользователя:

{
  "detail": "Нет прав доступа для выполнения действия"
}

Дополнительно клиент обрабатывает HTTP-статусы, указанные ранее в документе.

Ошибки валидации входных данных представлены следующей схемой:

{
  "detail": [
    {
      "loc": [
        "string"
      ],
      "msg": "string",
      "type": "string"
    }
  ]
}

Пример:

{
  "detail": [
    {
      "loc": [
        "body",
        "key"
      ],
      "msg": "ensure this value has at least 2 characters",
      "type": "value_error.any_str.min_length",
      "ctx": {
        "limit_value": 2
      }
    }
  ]
}
  • loc — указывает где именно случилась ошибка: в теле (payload) запроса в поле key
  • msg — сообщение, которые можно использовать для отладки или показа пользователю
  • type — служебное поле, указывающее на тип ошибки
  • ctx — дает больше контекста, например, минимально разрешенное количество символов для ввода

Строковые идентификаторы

В payload всегда возвращаются строковые идентификаторы. Некоторые языки, например, JavaScript, не поддерживают большие целые числа (Big Integers). API (де)сериализует целые числа в строки когда идентификаторы хранятся как int.

Decimal также представлены строками — причина описаны здесь.

Пагинация

Все методы коллекций реализуют limit & offset пагинацию. Разные варианты пагинации рассмотрены здесь.

Параметры запроса:

  • page — запрашиваемая страница результатов
  • max_per_page — максимальное кол-во записей на страницу

Пример ответа:

{
  "total_count": 0,
  "page": 0,
  "max_per_page": 0,
  "comments": [
      // ...
  ]
}
  • total_count — общее кол-во сущностей ресурса
  • page — выбранная страница

Лимитирование возвращаемых полей

Nice to have.

См. спецификацию JSON API — идея взята из нее.

Параметры запроса:

  • fields — список полей, которые клиент хочет получить от сервера (whitelist)
  • omit — список полей, которые нужно исключить из ответа сервера (blacklist)

expand — вернуть полный payload вложенных ресурсов:

GET /content/{id}?omit=content_type&expand=tags
GET /content/{id}?expand=tags.category — вложенность раскрывается через .

Приоритет параметров: omit > fields > expand.

Разделение ответственности

URL path в запросах используется для указания принадлежности, тело для передачи содержимого и заголовки для передачи метаданных. В некоторых ситуациях допускается передача информации, которая могла бы быть в заголовках, в параметрах запроса. Однако этим не стоит злоупотреблять. Заголовки – более гибкий механизм и могут передавать более важную служебную информацию, например, ID запроса.

Запросы и ответы должны быть адресованы конкретному ресурсу или коллекции. Запрос к коллекции всегда возвращает объекты этой коллекции. Вложенные сущности могут располагаться только уровнем ниже.

Фильтрация производиться только по атрибутам объекта коллекции.

Примеры моделей и фильтрации их сущностей в случае связи many-to-many: Category, Article, CategoryArticles.

GET /articles
GET /articles?category=food

GET /category_articles
GET /category_articles?category=food
GET /category_articles?article=1&article=2&article=3

Версионность

Чтобы избежать неожиданностей и ломающих изменения, клиент явно отправляет версию потребляемого API в каждом запросе.

Версионирование и переход между версиями может быть одним из наиболее сложных аспектов проектирования и эксплуатации API. Версии указываются в заголовках, например:

CMS-API-Version: 2021-02-03

Журнал изменений API (CHANGELOG) содержит только изменения, не совместимые с предыдущими версиями. Все изменения без обновления автоматически доступны для старых версий.

Обратно-совместимые изменения

  • Добавление новых API ресурсов
  • Добавление новых опциональных параметров запроса к существующим методам API
  • Добавление новых свойств / полей к существующим ответам API
  • Изменение порядка свойств / полей в существующих ответах API
  • Изменение длины или формата идентификаторов объектов или других неочевидных строк
  • Добавление новых типов событий, например, для веб-хуков

X-Headers

Кастомные X-заголовки API не поддерживаются.

Кэширование

Большинство запросов возвращают заголовок ETag. Многие запросы также возвращают заголовок Last-Modified. Значения этих заголовков могут быть использованы для последующих запросов к ресурсам с помощью заголовков If-None-Match и If-Modified-Since соответственно. Если ресурс не изменился, то сервер вернет 304 Not Modified.

Cache-Control: private, max-age=60 ETag: <hash of contents> Last-Modified: updated_at

Vary заголовок

Следующие значения заголовков должны быть объявлены в заголовке Vary: Accept, Authorization и Cookie.

Любой из этих заголовков может изменить представление данных и аннулирует закэшированную версию. Это может быть полезно, если пользователи имеют разные учетные записи для администрирования, каждая из которых имеет свои привилегии и видимость ресурсов.

Руководство

Сжатие

Все запросы поддерживают gzip.

Pretty printed ответы

Все JSON ответы отформатированы для удобства чтения и отладки.

Даты и часовые пояса

Дата и время явно представлены как ISO8601 timestamp с информацией о часовом поясе (DateTime в UTC).

Пример: 2014-02-27T15:05:06+01:00.

ISO 8601 UTC format: YYYY-MM-DDTHH:MM:SSZ.

HTTP rate limiting

Nice to have.

Все методы API ограничены в скорости. Текущий rate limit статус возвращается в заголовках HTTP всех запросов API.

Rate-Limit-Limit: 5000
Rate-Limit-Remaining: 4994
Rate-Limit-Reset: Thu, 01 Dec 1994 16:00:00 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Retry-After: Thu, 01 May 2014 16:00:00 GMT
 
RateLimit-Reset uses the HTTP header date format: RFC 1123 (Thu, 01 Dec 1994 16:00:00 GMT)

Статус ответа 429 Too Many Requests:

{
    "detail": "Превышен rate limit API."
}

CORS

Для XHR-запросов поддерживается CORS (Cross Origin Resource Sharing).

Принимаются домены проекта и партнеров.

$ curl -i https://api.akrisanov.ru -H "Origin: https://api.signl.ru"
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, OAuth-Scopes, Accepted-OAuth-Scopes
Access-Control-Allow-Credentials: false
 
// CORS Preflight request
// OPTIONS 200
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Requested-With
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE
Access-Control-Expose-Headers: ETag, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: false

TLS/SSL

Все API запросы поверх SSL, включая исходящие веб-хуки. Любой незащищенный запрос возвращает ssl_required, и ни один редирект не выполняется.

HTTP/1.1 403 Forbidden
Content-Length: 35
 
{
  "detail": "API запросы должны выполняться поверх HTTPS"
}

Уникальные идентификаторы запросов

Каждый запрос включает заголовок Request-Id для отладки в рамках сервисной архитектуры.

ID vs Keys

Ключами таблиц-справочников служат уникальные Ключи. Ключи являются человеко-читаемыми. Они могут быть представлены как первичными ключами БД, так и hash-значениями. Все запросы сущностей с one-to-one, one-to-many или many-to-many отношениями в качестве внешнего ключа должны принимать Ключ.

About

🤌🏻 Руководство по дизайну HTTP API сервисов

Topics

Resources

License

Stars

Watchers

Forks