Skip to content

Latest commit

 

History

History
509 lines (367 loc) · 28 KB

rest-api-guide.md

File metadata and controls

509 lines (367 loc) · 28 KB

REST API Best Practices

Цель гайда - улучшить разработку новых версий API и сделать его более единообразным для удобства потребителей.

Существующие контракты полностью сохраняются.

Работа над гайдом

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

  1. Описываем предлагаемые изменения в MR и скидываем в чат сообщества (на данный момент это ТГ)
    • Если изменения незначительные, достаточно получить апрувы (призываем как минимум техлидов команд) и принять реквест. Есть по прошествии недели реквест никто не посмотрел, то собираем встречу.
    • Если изменения требуют обсуждения, инициатор собирает встречу сообщества (как минимум техлидов)
  2. Для принятого пункта гайда кратко описываем почему тот или иной пункт появился

Общие рекомендации

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

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

HTTP methods

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.

$\textcolor{green}{\text{Рекомендуем:}}$

{
    "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"
}

$\textcolor{red}{\text{Не рекомендуем:}}$

{
    "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 уже зарегистрирован для этого юридического лица"
}

Aутентификация, авторизация и подпись

Аутентификация позволяет индентифицировать клиента 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 как для даты, так и для даты-времени.

$\textcolor{green}{\text{Рекомендуем:}}$

"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" 

$\textcolor{red}{\text{Не рекомендуем:}}$

"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 ), разделитель точка, количество разрядов зависит от валюты.

Необходимо всегда указывать код валюты.

$\textcolor{green}{\text{Рекомендуем:}}$

"currency": "RUB"
"country": "RU"
"amount": 1110.11

$\textcolor{red}{\text{Не рекомендуем:}}$

"currency": "643"
"currency": "Ruble"
"country": "RUS"
"country": "Belarus"
"sum": 1200.1

Фильтрация

Фильтры рекомендуем делать в виде строки запроса GET

$\textcolor{green}{\text{Рекомендуем:}}$

GET /api/sbp/v1/products?name=product&price=50&category=electronics

Если список аргументов в фильтре слишком длинный (URL может превысить 2048 символов), то рекомендуем использовать для реализации фильтров запрос POST с передачей аргументов в теле запроса.

$\textcolor{green}{\text{Рекомендуем:}}$

POST /api/sbp/v1/products/search

{
   dateFrom="...",
   dateTo="...",
   ....
}

Не рекомендуем передавать аргументы в теле запроса GET.

$\textcolor{red}{\text{Не рекомендуем:}}$

GET /api/sbp/v1/products
{
  "name": "product",
  "price": 50,
  "category": "electronics"
}

Реализация фильтров с логическими операциями or / not / ... по усмотрению исполнителя, общая рекомендация - избегать необходимости в таких фильтрах.

$\textcolor{red}{\text{Не рекомендуем:}}$

GET /api/sbp/v1/products?or_label=A&or_label=B&label_not=C

Передача массива параметров в запросе GET

Для передачи массива параметров в запросе GET рекомендуем передавать параметры через запятую либо через повторяющиеся переменные.

$\textcolor{green}{\text{Рекомендуем:}}$

GET /api/sbp/v1/products?ids=1,2,3

GET /api/sbp/v1/products?ids=1&ids=2&ids=3

$\textcolor{red}{\text{Не рекомендуем:}}$

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).

$\textcolor{green}{\text{Рекомендуем:}}$

GET /api/v1/products?sortBy=price&orderBy=desc

$\textcolor{red}{\text{Не рекомендуем:}}$

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.

$\textcolor{green}{\text{Рекомендуем:}}$

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"
}

Структура API

Методы

  • operationId - lowerCamelCase, содержит в себе короткое название метода (например, createOrder или getPaymentStatus).
  • Тело запроса и ответа должны быть вынесены в блок Components Object как Schema.

Объекты

  • Объекты запросов и ответов именуются стилем CamelCase с постфиксами Request/Response (например, CreateOrderRequest). Исключение составляет случай, когда модель присутствует и в запросе, и в ответе. Тогда постфикс опускается.
  • Все объекты, которые используются больше одного раза, должны быть вынесены как отдельные модели/параметры. Их использование происходит с помощью ссылок $ref.

Enum и mapping для discriminator

  • Перечисления и значения маппинга для дискриминатора именуются UPPER_SNAKE_CASE