- REST API Best Practices
Цель гайда - улучшить разработку новых версий API и сделать его более единообразным для удобства потребителей.
Существующие контракты полностью сохраняются.
В процессе разработки или ревью API может возникнуть ситуация когда мы понимаем что гайд либо устарел, либо противоречит здравому смыслу. В этом случае:
- Описываем предлагаемые изменения в MR и скидываем в чат сообщества (на данный момент это ТГ)
- Если изменения незначительные, достаточно получить апрувы (призываем как минимум техлидов команд) и принять реквест. Есть по прошествии недели реквест никто не посмотрел, то собираем встречу.
- Если изменения требуют обсуждения, инициатор собирает встречу сообщества (как минимум техлидов)
- Для принятого пункта гайда кратко описываем почему тот или иной пункт появился
REST (Representational State Transfer) - это архитектурная абстракция, которая описывает взаимодействией компонентов в распределенной системе. REST широко используется для создания web API (RESTful web API), которое использует протокол HTTP как транспорт.
В RESTful web API обращение к ресурсу происходит по уникальному идентификатору (URL), с указанием действия над ресурсом (HTTP method), в запросе может передаваться тело запроса (body) и дополнительные заголовки (HTTP header fields). В ответ на запрос, ресурс возвращает код ответа (HTTP status code) и может возвращаться тело ответа.
В качество формата тела запроса/ответа рекомендуется использовать только JSON. Использование других форматов (XML/TXT) возможно в исключительных случаях.
Мы ориентируемся на Richardson Maturity Model, но считаем избыточным Level 3
Используем для именования параметров camelCase
URL формируется по шаблону https://{FQDN}/api/{path}. Рекомендуем использовать kebab case в наименовании пути.
URL состоит из следующих частей:
{FQDN} - полное имя домена
/api - Префикс для отличия API от документации, лендинга и т.п.
/{path} - Путь. Путь может включать версию. Мы рекомендуем REST подход к формированию пути основанному на ресурсах (см. https://restfulapi.net/resource-naming/)
Других префиксов быть не должно.
Среды разделены по доменам:
Например,
https://service-dev.example.ru - тестовая среда. Для внутренних тестов.
https://service-test.example.ru - превью, песочница для партнеров.
https://service.example.ru - прод.
В результате URL может выглядеть так:
https://pay.raif.ru/api/fiscal/v2/customer-receipts
GET - используется только для получения данных от сервера, не изменяет данные на сервере и не должен иметь тела запроса.
POST - используется для создания новых ресурсов (см другие варианты использования), генерация идентификатора нового ресурса выполняется на стороне сервера. В общем случае POST не гарантирует идемпотентность.
PUT, PATCH - используются для изменения существующего ресурса на стороне сервера, PUT полностью замещает старую сущность и при его использовании в теле запроса нужно передавать все поля сущности, PATCH выполняет частичное изменение, при его использовании в теле запроса достаточно передать изменяемые поля.
DELETE - удаляет ресурс на стороне сервера.
Признаком ошибки являются коды HTTP status code 4xx и 5xx. Не следует возвращать код HTTP 200 для возврата ошибок.
За исключением коммуникационных ошибок типа 502 Bad Gateway, необходимо детализировать ошибку в теле ответа.
Не следует дублировать HTTP status code в теле сообщения.
Поле “code” содержит код ошибки вида SOME_ERROR_DETAIL (SCREAMING_SNAKE_CASE style). Рекомендуем посмотреть какие коды ошибок уже используются, чтобы не дублировать уже существующие коды с новым именем.
Поле "message" - это человеко читаемое поле, содержащие описание ошибки, которые можно выводить пользователю.
Ошибка валидации запроса необходимо возвращать в виде массива в поле "errors".
Если необходим структурированный ответ об ошибке валидации, то допустимо включать в поле "errors" вложенные объекты или массивы.
Рекомендуем возвращать traceId для трассировки в теле ответа, чтобы упростить поиск ошибки при взаимодействии с внешними контрагентами. Предпочительный формат B3.
{
"code": "RECEIPT_VALIDATION_FAILED",
"message": "Чек не прошел валидацию. Причина: {value}",
"traceId" : "80f198ee56343ba8"
}
{
"code": "DATA_ERROR",
"message": "Ошибки в полях запроса",
"errors":
{
"username": "Описание ошибки",
"email": "Не прошла валидация почты"
"password": "Не заполнено",
"items": [
{
"amount": "Поле должно быть заполнено",
"quantity": "число вне допустимого диапазона (ожидалось <5 разрядов>.<3 разрядов>"
}
]
},
"traceId": "a123ewr"
}
{
"code": "RECEIPT_VALIDATION_FAILED",
"message": "Чек не прошел валидацию. Причина: items[0].amount число вне допустимого диапазона (ожидалось <8 разрядов>.<2 разрядов>), items[0].quantity число вне допустимого диапазона (ожидалось <5 разрядов>.<3 разрядов>)",
"traceId": "a123ewr"
}
{
"timestamp": "2021-01-01T12:00:27.87+00:20",
"status": 401,
"error": "Unauthorized",
"message": "",
"path": "/api/cards/v1/callback/settings/"
}
200 OK
{
"code": "ERROR.ACCOUNT_ALREADY_REGISTERED",
"message": "Счет 40700000007721511123 уже зарегистрирован для этого юридического лица"
}
Аутентификация позволяет индентифицировать клиента API, авторизация разрешает клиенту определенные действия.
Рекомендуется выполнять аутентификацию всех запросов с помощью Bearer токенов.
Bearer токен передается в заголовке:
Authorization: Bearer <token>
Если для сервиса критична надежная аутентификация, то рекомендуем использовать взаимную аутентификацию TLS с длинной ключа не менее 2048 бит. В отдельных случаях, допустимо ограничивать доступ к сервису по белому списку IP-адресов.
Для OpenAPI авторизация должна быть определена на уровне Security Scheme, если это возможно.
Если аутентификация не пройдена, то следует ответить кодом 401. Аутентификация считается неуспешной в случае:
- Если токен не декодируется
- Если подпись токена невалидна
- Если пользователь по токену не идентифицирован
Если аутентификация пройдена, но дейстие не авторизовано, то следует ответить кодом 403. Неуспешной авторизацией считается если у пользователя не достаточно разрешений для выполнения метода Если выполнен запрос данных не принадлежащих пользователю, то следует ответить кодом 404.
Необходимо использовать HTTPS для обеспечения безопасного обмена данными между клиентом и сервером. Используйте ролевую модель авторизации для определения прав доступа пользователей к различным ресурсам API. Обеспечьте механизмы контроля доступа для предотвращения несанкционированного доступа к API. Например, можно использовать IP-адреса, ограничить доступ по времени или на основе других параметров.
Обратные вызовы к клиентам (callback) следует осуществлять через POST запросы с телом в виде JSON. Сообщения должны быть подписаны с помощью алгоритма HMAC-SHA-256 с использованием общего секретного ключа, подпись формируется для полного финального тела ответа. Подпись передается в заголовке x-api-signature-sha256.
Для аутентификации рекомендуется использовать Bearer-токен, не рекомендуется HTTP Basic Authorization или полное отсутствие авторизации. Необходимо использовать HTTPS для обеспечения безопасного обмена.
В начале пути указывается версия API продукта, и она не меняется, пока не выпустили новую версию продукта целиком:
https://pay.raif.ru/api/sbp/v1/companies/Romashka/...
Логически несвязанные API продуктов версионируются раздельно.
В REST API изменения могут быть ломающими (breaking changes) или обратно-совместимыми (backward-compatible changes) в зависимости от того, будут ли они нарушать существующий контракт API.
Ломающие изменения нарушают контракт API и могут привести к тому, что клиенты, которые ранее работали с API, перестанут работать или будут работать некорректно. Например, если изменить тип возвращаемого значения метода API, это может привести к ошибке на стороне клиента, который ожидал другой тип данных.
Обратно-совместимые изменения не нарушают контракт API и не приводят к сбоям на стороне клиента. Такие изменения могут включать добавление новых ресурсов, методов, параметров запросов, но не изменение или удаление существующих.
Примеры обратно-совместимых изменений:
- добавление новых ресурсов или методов;
- добавление дополнительных параметров в запрос;
- добавление нового заголовка;
- изменение порядка полей в ответе;
- изменение порядка сортировки в ответе.
Некоторые примеры ломающих изменений:
- изменение в стандартном заголовке ответа или кастомном заголовке;
- изменение бизнес-логики (например, в документации описано поведение метода, но внезапно метод возвращает другой результат при неизменных входных данных);
- изменение типа данных параметра или полей в ответе;
- удаление ресурса или метода из API;
- изменение регистра значений в ответе (например, "OK" вместо "ok").
Для ломающих измененений должна создаваться новая версия ресурса.
Важно помнить, что любые изменения в API должны быть хорошо продуманы и документированны, чтобы избежать проблем совместимости и сбоев у потребителей API.
Используем формат ISO 8601. Избегаем использования времени / микросекунд / миллисекунд если в этом нет явной необходимости. Принимаем в запросах время и в UTC, и с указанием чаcового пояса, возвращаем в ответе строго в формате UTC. Если в запросе нет признака UTC или указания часового пояса - возвращаем ошибку валидации.
Паттерны для даты-времени YYYY-MM-DDThh:mm:ssZ / YYYY-MM-DDThh:mm:ss±hh:mm.
Паттерн для даты YYYY-MM-DD.
Используем названия полей вида someData как для даты, так и для даты-времени.
"birthDate": "1980-01-30"
"qrExpirationDate": "2023-07-22T09:14:38+03:00"
"createDate": "2019-08-24T14:15:22Z"
"transactionDate": "2022-12-08T13:21:04.631543+03:00"
"birthday": "1980.01.30"
"dateTime": "2020-01-15T16:01:49.043924"
"createDateTime": "2011-03-01T14:15:22Z"
Формат стран ISO 3166, UPPER ALPHA-2.
Формат валют ISO 4217, UPPER ALPHA-3.
Суммы указываются как строка с дробным числом (если количество разрядов >0 ), разделитель точка, количество разрядов зависит от валюты.
Необходимо всегда указывать код валюты.
"currency": "RUB"
"country": "RU"
"amount": 1110.11
"currency": "643"
"currency": "Ruble"
"country": "RUS"
"country": "Belarus"
"sum": 1200.1
Фильтры рекомендуем делать в виде строки запроса GET
GET /api/sbp/v1/products?name=product&price=50&category=electronics
Если список аргументов в фильтре слишком длинный (URL может превысить 2048 символов), то рекомендуем использовать для реализации фильтров запрос POST с передачей аргументов в теле запроса.
POST /api/sbp/v1/products/search
{
dateFrom="...",
dateTo="...",
....
}
Не рекомендуем передавать аргументы в теле запроса GET.
GET /api/sbp/v1/products
{
"name": "product",
"price": 50,
"category": "electronics"
}
Реализация фильтров с логическими операциями or / not / ... по усмотрению исполнителя, общая рекомендация - избегать необходимости в таких фильтрах.
GET /api/sbp/v1/products?or_label=A&or_label=B&label_not=C
Для передачи массива параметров в запросе GET рекомендуем передавать параметры через запятую либо через повторяющиеся переменные.
GET /api/sbp/v1/products?ids=1,2,3
GET /api/sbp/v1/products?ids=1&ids=2&ids=3
GET /api/sbp/v1/products?ids=["1", "2", "3"]
GET /api/sbp/v1/products
{
"ids": [1, 2, 3]
}
GET /api/sbp/v1/products?status[]=1&status[]=2&status[]=3
GET /api/sbp/v1/products?ids=1-3
В общем случае, клиент не должен полагаться на сортировку результата сервером, за исключением случаев, когда документация на API явно описывает сортировку по-умолчанию.
В случаях когда запрос возвращает небольшую выборку, более предпочтительным вариантом может оказаться сортировка на стороне клиента.
Если требуется сортировка по одному полю, то рекомендуем указывать поле для сортировки в параметре sortBy и порядок сортировки в параметре orderBy (asc/desc).
GET /api/v1/products?sortBy=price&orderBy=desc
GET /api/v1/users?sort=-created_at,+username
GET /api/v1/products?sortby=price_asc&date_desc
GET /api/v1/users?sort={"created_at":"desc","username":"asc"}
Если параметр orderBy не указан, то по-умолчанию предполагается сортировка по возрастанию (asc).
В сложных случаях, когда требуется сортировка по нескольким полях в сочетании со сложными фильтрами и(или) пагинацией, рекомендуем использовать метод POST.
POST /api/v1/users/search
{
"filter": "some filter",
"paging": {
"offset": 50,
"limit": 20
},
"order": [
{ "priority": 1,
"sortBy": "created_at",
"orderBy": "desc"
},
{ "priority": 2,
"sortBy": "username",
"orderBy": "asc"
}
]
}
Для небольших объемов данных, которые редко изменяются, рекомендуется использовать offset-пагинацию (page, size). Параметр запроса "page" указывает на 1-based номер страницы, "size" - на максимальный размер возвращаемого массива.
Запрос:
GET /api/sbp/v1/products?name=potato&page=2&size=20
В случае сложных фильтров:
POST /api/sbp/v1/products/search
{
"filter": "some filter",
"paging": {
"page": 2,
"size": 20
}
}
Ответ кроме самих данных содержит общее число страниц, число элементов и флаг наличия следующей страницы:
{
"content": ["foo", "bar"],
"totalPages": 2,
"totalElements": 21,
"last": true
}
Для больших или быстро меняющихся наборов данных, лучше использовать курсор-пагинацию. Для значения курсора следует использовать уникальный индексированный набор полей. Например, дату создания записи. Для унификации значение курсора кодируется в base64.
Запрос первой страницы может быть без явного указания курсора:
GET /api/sbp/v1/products?limit=20
Ответ должен содержать значение курсора начала следующей страницы:
{
"content": [],
"nextCursor": "ewogICJjcmVhdGVkIjogIjIwMjMtMDctMjJUMDk6MTQ6MzgrMDM6MDAiCn0="
}
Запрос следующей страницы со значеним курсора:
GET /api/sbp/v1/products?cursor=ewogICJjcmVhdGVkIjogIjIwMjMtMDctMjJUMDk6MTQ6MzgrMDM6MDAiCn0%3D&limit=20
Если записей больше нет, nextCursor должен быть пустым:
{
"content": [{
"id": 123
}],
"nextCursor": ""
}
При необходимости сортировки данных, их следует добавлять в курсор. Запрос:
GET /api/sbp/v1/products?sortBy=price,name&limit=20
Ответ:
{
"content": [{
"id": 123
}],
"nextCursor": "ewogICJwcmljZSI6IDEyLjAxLAogICJuYW1lIiwgInBvdGF0byIsCiAgImNyZWF0ZWQiOiAiMjAyMy0wNy0yMlQwOToxNDozOCswMzowMCIKfQ"
}
operationId
- lowerCamelCase, содержит в себе короткое название метода (например,createOrder
илиgetPaymentStatus
).- Тело запроса и ответа должны быть вынесены в блок Components Object как Schema.
- Объекты запросов и ответов именуются стилем CamelCase с постфиксами
Request/Response
(например,CreateOrderRequest
). Исключение составляет случай, когда модель присутствует и в запросе, и в ответе. Тогда постфикс опускается. - Все объекты, которые используются больше одного раза, должны быть вынесены как отдельные модели/параметры. Их использование происходит с помощью ссылок
$ref
.
- Перечисления и значения маппинга для дискриминатора именуются UPPER_SNAKE_CASE