Ensi API Design Guide

Общие правила

  1. Для реализации API используется архитектурный стиль REST

  2. Форматом передачи данных ДОЛЖЕН быть json

  3. Название ресурса в эндпоинте ДОЛЖНО быть во множественном числе (POST /v1/users, а не POST /v1/user), за исключением тех случаев когда ресурс может существовать только в единственном числе (/v1/profile)

  4. Название ресурса в эндпоинте ДОЛЖНО быть в spiral-case.

  5. Параметры в query и поля в body ДОЛЖНЫ быть в snake_case.

  6. Идентификатор версии API всегда ДОЛЖЕН присутствовать в урле, например POST /v1/users

  7. Несуществующие страницы API ДОЛЖНЫ отдавать 404 ошибк и json ответ соответсвующий формату описанному в разделе “Формат ответа” c code: "NotFoundHttpException"

Формат полей

  1. Все целочисленные идентификаторы сущностей ДОЛЖНЫ иметь тип integer.

  2. Datetime поля должны передаваться строкой в формате ISO-8601 в UTC. Например “updated_at”: “2020-01-01T15:47:21.000000Z” В OpenApi такое поле описывается как type: string, format: date-time

  3. Даты (без времени) должны передаваться строкой в формате ISO-8601 full date
    Например “birhtday”: “1990-01-25” В OpenApi такое поле описывается как type: string, format: date

  4. Цены должны передаваться в копейках, с типом integer

Формат ответа

Тело ответа ДОЛЖНО содержать только следующие поля:

data - основной объект ответа, может иметь следующие типы:

  • null;

  • object - в случае, если запрашивается одна сущность, например, запрос по id;

  •  array - в случае, если запрашивается список сущностей, каждый элемент массива представляет собой отдельную конкретную сущность.

errors - необязательный массив ошибок запроса, каждый элемент в массиве содержит:

  • code - обязательный строковый код ошибки, скорее всего берется из фиксированного списка. В документации к api ДОЛЖЕН присутствовать enum, в котором перечислены все коды ошибок;

  • message -  обязательное строковое описание ошибки;

  • meta - опциональный объект с дополнительными метаданными ошибки.

meta - необязательный объект с дополнительной информацией о запросе, например для передачи отладочной информации или информации для пагинации. Каждый компонент, который хочет добавить данные в этот объект, должен добавлять их через дополнительное поле, например: meta.pagination.* В корень объекта meta нельзя добавлять информацию для исключения конфликта в названии данных разных компонентов.

Стандартные методы

Стандартные методы позволяют реализовать CRUD функциональность для ресурса, которой достаточно в большом числе случаев.
API не обязано реализовывать все стандартные методы для всех ресурсов.

Метод

HTTP реализация

Цель

Метод

HTTP реализация

Цель

Get

GET <resource-url>/<id>

Получение объекта по id

Create

POST <resource-url>

Создание объекта

Replace

PUT <resource-url>/<id>

Обновление всех полей объекта

Patch

PATCH <resource-url>/<id>

Обновление указанных полей объекта

Delete

DELETE <resource-url>/<id>

Удаление объекта

Search

POST <resource-url>:search

Посик объектов по фильтрам

SearchOne

POST <resource-url>:search-one

Быстрый поиск одного объекта по фильтру

где <resource-url> например /api/v1/users.

Стандартные методы: Get

Формат запроса:

GET /api/v1/users/1006779?include=roles,roles.rights

параметр include является опциональным

Формат ответа:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "data": { "id": 1006779, "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z", "roles": [ { "id": 1, "code": "admin", "rights": [ { "id": 2, "code": "delete_posts" } ] } ] } }

 

Стандартные методы: Create

Формат запроса:

POST /api/v1/users

1 2 3 4 { "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z" }

 

Формат ответа:

1 2 3 4 5 6 7 { "data": { "id": 1006779, "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z" } }

Объект в data полностью соответствует объекту в методе Get без параметров.

Стандартные методы: Replace

Формат запроса:

PUT /api/v1/users/1006779

1 2 3 4 { "name": "John Doe", "last_login_at": null }

Запрос ДОЛЖЕН являться идемпотентным. Таким образом в data могут отсутствовать только необязательные поля, которые в этом случае будут сброшены до значения по умолчанию. Поле id из data ДОЛЖНО игнорироваться.

Формат ответа:

1 2 3 4 5 6 7 { "data": { "id": 1006779, "name": "John Doe", "last_login_at": null } }

Объект в data полностью соответствует объекту в методе Get без параметров.

Стандартные методы: Patch

Формат запроса:
PATCH /api/v1/users/1006779

1 2 3 { "name": "John Doe" }

Только поля указанные в data ДОЛЖНЫ быть изменены.  Поле id из data ДОЛЖНО игнорироваться.

Формат ответа:

1 2 3 4 5 6 7 { "data": { "id": 1006779, "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z" } }

 

Объект в data полностью соответствует объекту в методе Get без параметров.

Стандартные методы: Delete

Формат запроса:

DELETE /api/v1/users/1006779

Формат ответа:

1 2 3 { "data": null }

 

Если объект уже был удален до этого это не должно приводить к 404 ошибке. Аналогичным образом должны вести себя дополнительные “удаляющие методы”. Например, удаление файла привязанного к объекту.

Формат запроса:

POST /api/v1/users:search

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "sort": [ "-last_login_at", // по убыванию last_login_at "id" ], "filter": { "id": [ 12125, 1006779 ], "last_login_after": "2020-01-01T15:47:21.000000Z" }, "include": [ "roles" ], "pagination": { ... } }

Все поля являются опциональными.

Формат ключа и значения фильтра никак не стандартизирован. Внутри API каждый фильтр обрабатывается по своему.

Подмножество экземпляров ресурса отфильтрованных сложным образом можно также выносить в отдельный ресурс и отдельный endpoint для него соответственно. Для них также можно делать отдельный фильтр.

Запрос использует POST чтобы избежать некоторых ограничений, связанных с реализацией параметров в GET запросах как в самом протоколе HTTP, так и в OpenApi генераторах.

Возможные форматы пагинации в запросе и ответе описаны в разделе посвященном пагинации.

 

Формат ответа:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "data": [ { "id": 1006779, "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z", "roles": [ { "id": 1, "code": "admin" } ] } ], "meta": { "pagination": { ... } } }

Стандартные методы: SearchOne

Формат запроса: полностью совпадает с форматом метода Search

Формат ответа:

1 2 3 4 5 6 7 { "data": { "id": 1006779, "name": "John Doe", "last_login_at": "2020-01-01T15:47:21.000000Z" } }

Объект в data полностью соответствует объекту в методе Get без параметров.

Дополнительные методы

При проектировании API СЛЕДУЕТ стараться ограничиваться стандартными методами, которых обычно достаточно для большинства задач, однако при необходимости можно добавлять и дополнительные.

Пользовательские методы всегда ДОЛЖНЫ использовать POST и образовываются добавлением названия метода к <resource url> через двоеточие.

Форматы тела запроса и ответа не регламентируются за исключением общих правил для формата ответа, описанных в начале этого руководства.

В любом случае СЛЕДУЕТ проектировать их похожими на стандартные методы.

 

Пример запроса ко всему ресурсу:

POST /api/v1/users:mass-delete

1 2 3 {   "id": [1, 2, 3] }


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

1 2 3 {   "data": null }


Пример запроса к элементу ресурса:

POST /api/v1/offer-certificates/5:upload-file

 

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

1 2 3 4 5 6 7 { "data": { "url": "https://.../offers/certs/25/c5/test.pdf", "path": "certs/25/c5/test.pdf", "diskType": "public" } }

Пагинация

Методы API отдающие списки элементов ресурсов ДОЛЖНЫ поддерживать пагинацию.

Пример такого метода - стандартный метод Search.

СЛЕДУЕТ поддерживать сразу два способа пагинации:

Offset-based pagination

Запрос:

1 2 3 4 5 6 7 8 { ... "pagination": { "type": "offset" "offset": 40, //nullable "limit": 20 } }

Ответ:

1 2 3 4 5 6 7 8 9 10 { "meta": { "pagination": { "offset": 40 "limit": 20, "total": 253, // с учётом фильтров "type": "offset" } } }

 

Cursor-based pagination

Запрос:

1 2 3 4 5 6 7 8 { ... "pagination": { "type": "cursor" "cursor": "eyJpZCI6MTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", //nullable "limit": 20 } }

Ответ:

1 2 3 4 5 6 7 8 9 10 11 { "meta": { "pagination": { "cursor": "eyJpZCI6MTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", // nullable "limit": 20, "next_cursor": "eyJpZCI6MjEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX1", // nullable "previous_cursor": "eyJpZCI6MTIsIl9wb2ludHNUb05leHRJdGVtcyI6ZmFsc2V9", // nullable "type": "cursor" } } }

Почитать какой вариант пагинации в каких случаях предпочтительнее для потребителя можно, например, здесь

 

Если объект пагинации в запросе не задан, то по-умолчанию СЛЕДУЕТ считать

1 2 3 4 5 6 7 { "pagination": { "type": "offset", "offset": 0, "limit": 10 } }

Если limit не указан в запросе, то СЛЕДУЕТ брать limit по-умолчанию - 10

Если limit указан равным 0, то СЛЕДУЕТ возвращать 0 элементов

Если limit указан отрицательным, то СЛЕДУЕТ выводить все элементы без учёта offset/cursor

Для предотвращения высоких нагрузок на API допускается следующее:

  • отключение offset пагинации в конкретном методе

  • отключение возможности получить все элементы без пагинации через limit = -1 в конкретном методе. В этом случае возвращается 0 элементов как при limit = 0

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

Подресурсы

Использование подресурсов (например GET /customers/1/addresses/5) может сделать API более читабельным, но имеет ряд неприятных последствий

  1. Получаем менее унифицированный формат описания ресурса. В ответе методов Get и Search должен быть customer_id, а методах создания и обновления - нет

  2. При смене связи подресурса с родительским (перепривязки адреса к другому покупателю) меняется URL ресурса, старый перестает работать

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

  4. При высокой вложенности все преимущества читабельности исчезают.

В общем случае использование подресурсов НЕ РЕКОМЕНДУЕТСЯ, следует сразу заводить отдельные ресурсы.

 

Исключения

  1. Идентификатор ресурса не является уникальным. Уникальной является лишь пара идентификаторов {parent_id, child_id}

  2. Если выполняется всё из нижеперечисленного:

    1. Есть явная иерархия и существование ресурса невозможно без существования другого родительского ресурса

    2. Работа с ресурсом вне контекста родителя не имеет смысла

    3. Связь ресурса с родительским ресурсом постоянна

В любом случае вложенность подресурсов ДОЛЖНА быть не более 2.

Если используется модульный подход к реализации апи, то в начало урла СЛЕДУЕТ добавлять название модуля, например все эндпоинты модуля Customers стоит начинать с customers, например

/api/v1/customers/addresses.

Это не считается подресурсом.

Загрузка файлов

Загрузку крупных файлов СЛЕДУЕТ осуществлять отдельным POST запросом с   

Content-Type: multipart/form-data

POST /api/v1/offer-certificates/5:upload-file

HTTP status codes

API ДОЛЖНО использовать только следующие HTTP коды ответа:

  • 200 OK во всех ситуациях, когда запрос не заканчивается ошибкой

  • 201 Created если его генерирует ваш фрейморк автоматически. В противном случае РЕКОМЕНДУЕТСЯ использовать 200

  • 401 Unauthorized

  • 403  Forbidden

  • 404 Not found при запросе несуществующего ресурса или экземпляра ресурса

  • 400 Bad Request при любых других ошибках, причиной которых является клиент

  • 500 Internal Server Error - при любой ошибке приложения, причиной которой является проблемы в самом приложении, а не в запросе.

В документации API у каждого эндпоинта ДОЛЖНЫ быть указаны все возможные коды ответа.

Версионирование API

Что делать если нужно внести изменения в API ломающие обратную совместимость?

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

  2. Если такой вариант недоступен, то используем принцип эволюции апи (почитать можно тут

https://apisyouwonthate.com/blog/api-evolution-for-rest-http-apis) накопившийся из-за этого технический долг со временем чистим.

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

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

Инкрементируем версию (/v1/ -> /v2/), для старой устанавливаем Sunset заголовок с датой выключения.

Идентификатор версии всегда ДОЛЖЕН присутствовать в url.

Файлы спецификации API (openapi 3.0), а также контроллеры реализующие непосредственно методы API СЛЕДУЕТ делать полностью независимыми у разных версий.

Документация API

Все эндпоинты API ДОЛЖНЫ быть задокументированы через Open Api Specification 3.0.

Документация ДОЛЖНА быть доступна через браузер используя Swagger UI.

API СЛЕДУЕТ реализовывать используя Design-first подход.

Документацию всех версий API сервиса СЛЕДУЕТ держать в общем сваггере. Конкретную версию API выбираем в селекте Servers. Версии в нём СЛЕДУЕТ располагать в порядке убывания.

При написании OAS3 спецификаций СЛЕДУЕТ придерживаться требований описанных здесь и здесь.

 

 

 


Дополнительные требования к API

В каждом сервисе ДОЛЖНА присутствовать поддержка трассировки запросов, соответствующая стандарту opentracing. TraceId передается через http-заголовок.