Данное руководство описывает дизайн 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.
Список контента:
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 | 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"}
]
Статус | Описание |
---|---|
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-заголовки 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
: Accept
, Authorization
и Cookie
.
Любой из этих заголовков может изменить представление данных и аннулирует закэшированную версию. Это может быть полезно, если пользователи имеют разные учетные записи для администрирования, каждая из которых имеет свои привилегии и видимость ресурсов.
Все запросы поддерживают gzip.
Все JSON ответы отформатированы для удобства чтения и отладки.
Дата и время явно представлены как ISO8601 timestamp с информацией о часовом поясе (DateTime в UTC).
Пример: 2014-02-27T15:05:06+01:00.
ISO 8601 UTC format: YYYY-MM-DDTHH:MM:SSZ.
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."
}
Для 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
Все API запросы поверх SSL, включая исходящие веб-хуки. Любой незащищенный запрос возвращает ssl_required
, и ни один редирект не выполняется.
HTTP/1.1 403 Forbidden
Content-Length: 35
{
"detail": "API запросы должны выполняться поверх HTTPS"
}
Каждый запрос включает заголовок Request-Id
для отладки в рамках сервисной архитектуры.
Ключами таблиц-справочников служат уникальные Ключи. Ключи являются человеко-читаемыми. Они могут быть представлены как первичными ключами БД, так и hash-значениями. Все запросы сущностей с one-to-one, one-to-many или many-to-many отношениями в качестве внешнего ключа должны принимать Ключ.