RESTаврация API
Перемещаясь между проектами, я часто вижу одни и те же ошибки при построении REST API, которых легко избежать. Эта статья — попытка структурировать моё понимание того, каким должен быть хороший REST API.
Если написанное здесь для вас не станет новым и вы так и делали, то вы красавчик, продолжайте в том же духе.
Зачем заморачиваться?
Если в проекте работает один фуллстек, который делает и фронт, и бэк, то действительно особо незачем. Если же разработчиков несколько и нанимаются новые, то последовательный и понятный REST API упростит разработку и ввод новых сотрудников.
Ресурс
Ресурс — это то, что хочет получить фронтенд или любой другой пользователь API. Основной строительный блок REST.
Ресурс следует называть существительным во множественном числе, если таких ресурсов может быть несколько. Название должно быть коротким и понятным, в идеале одно слово без аббревиатур и сокращений.
Если для однозначного понимания всё-таки требуется несколько слов, то для разделения следует использовать символ-разделитель (дефис или подчёркивание). Это позволяет держать URL в нижнем регистре, который проще читается и позволяет избежать разночтений.
| Плохо | Хорошо |
|---|---|
license |
licenses |
individualentity |
individuals |
map-coordinate |
coordinates |
competencyMatrix |
competency-matrices |
Домен
Домен — это ресурс верхнего уровня. Если ресурс является дочерним, то путь следует сделать вложенным.
Если эндпоинты имеют общий префикс, то следует выделить для них домен и переместить повторения туда.
| Плохо | Хорошо |
|---|---|
/individual/card-block |
/individuals/card/block |
/individual/card-info |
/individuals/card/info |
Параметры
Для получения конкретного ресурса или выборки ресурсов по фильтрам в запрос передаются параметры. Параметры могут передаваться:
- в пути (path) — в адресе ресурса;
- в запросе (query) — после адреса ресурса и отделяются от него вопросительным знаком;
- в теле (body) — внутри HTTP-запроса. Поддерживается не всеми методами.
В одном API для параметров всех видов следует использовать одинаковое разделение слов. Фронтендерам приятнее, когда это camelCase. Параметры следует называть ёмко, не стоит повторять в них название сущностей.
Параметры пути лучше подходят для конкретных ресурсов, а параметры запроса для фильтров и настройки формата вывода.
| Плохо | Хорошо |
|---|---|
/individual/search/{query} |
/individuals?q={query} |
/legal/organizations/type/{type} |
/legal/organizations?type=KT |
Избегайте лишних идентификаторов в пути. Если дочерний ресурс уже имеет собственный уникальный идентификатор, дополнительный ID родителя часто оказывается избыточным. Скорее всего связь уже хранится в базе данных.
| Плохо | Хорошо |
|---|---|
/users/{user_id}/photos/{photo_id} |
/photos/{photo_id} |
null или undefined
Разделяйте наличие отсутствия и отсутствие наличия. В JSON есть тип null, который означает «значение отсутствует». null подходит, когда в поле нужно записать пустое значение, например стереть email-пользователя.
Если поле не должно измениться, следует не заполнять это поле и выкинуть его из запроса, не присваивая ему никаких значений, даже null.
Методы
Название HTTP-метода говорит о действии, производимом с ресурсом.
GET
Чтение ресурса. GET — безопасный и идемпотентный метод: повторные одинаковые запросы не меняют состояние сервера.
Хотя тело в GET-запросе спецификация и не запрещает, большинство серверов не читают его. Для передачи параметров используйте параметры пути или запроса.
| Код ответа | Ситуация |
|---|---|
| 200 | Ресурс найден |
| 304 | Ресурс не отличается от кэша |
| 404 | Ресурс не найден |
HEAD
Чтение заголовков ресурса. Похож на GET, разница только в том, что на HEAD не отправляется тело ответа.
Используются, чтобы оценить размер ответа на GET-запрос или проверить существование ресурса.
| Код | Ситуация |
|---|---|
| 200 | Ресурс найден |
| 304 | Ресурс не отличается от кэша |
| 404 | Ресурс не найден |
POST
Создание ресурса или выполнение другого действия, для которого не хватило встроенных глаголов. Не гарантирует идемпотентность, может вернуть ошибку при повторном выполнении, а может создать второй ресурс.
Для передачи параметров используйте путь и тело запроса. В некоторых компаниях, особо сильно замороченных на теме безопасности, предпочитают использовать POST даже для получения фильтрованных списков, чтобы не передавать персональные данные в query-параметрах, а передавать в теле запроса. Это избыточно, так как TLS защищает и строку запроса при передаче по сети, но оправдано, если вы боитесь утечки access-логов.
POST по смыслу подходит и для запуска асинхронных действий, которые потом отслеживаются лонг-поллингом или подпиской.
| Код | Ситуация |
|---|---|
| 201 | Ресурс создан |
| 400 | Переданы некорректные значения |
PUT
Полное обновление ресурса. Гарантирует идемпотентность, повторный запрос не должен менять результат по сравнению с первым.
Для передачи параметров используйте путь и тело запроса. Обычно PUT используют для полной замены ресурса по URI. Если отдельное поле ресурса имеет свою бизнес-логику, его можно вынести в отдельный подпуть и обновлять через PUT.
PUT /users/{user_id}/status
"active"
Может использоваться для идемпотентного создания ресурса: если клиент знает идентификатор, то может вызвать PUT на /documents/{id}. Повторный такой запрос перезапишет ресурс, а не создаст новый.
| Код | Ситуация |
|---|---|
| 200 | Ресурс обновлён, приложен в теле |
| 204 | Ресурс обновлён, но повторно не отправляется |
| 400 | Переданы некорректные значения |
PATCH
Частичное обновление ресурса. Не обязан быть идемпотентным, зависит от семантики операции. Например, PATCH может описывать операцию увеличения счётчика на единицу; в таком случае повторный запрос изменит результат ещё раз.
Для передачи параметров используйте путь и тело запроса.
PATCH /users/{user_id}
{"status":"active"}
| Код | Ситуация |
|---|---|
| 200 | Ресурс обновлён, в теле приложен целиком |
| 204 | Ресурс обновлён, но повторно не отправляется |
| 400 | Переданы некорректные значения |
DELETE
Удаление ресурса. Метод считается идемпотентным: повторный запрос может вернуть 404, 200 или 204, но состояние сервера после первого удаления не изменится.
С телом запроса у DELETE ситуация такая же, как у GET: формально не запрещено, но почти никто не поддерживает. Используйте параметры пути и запроса.
| Код | Ситуация |
|---|---|
| 200 | Ресурс удалён |
| 204 | Ресурс удалён без тела ответа |
| 400 | Переданы некорректные значения |
Именование действий
HTTP-методы — это глаголы. Если действие эндпоинта совпадает с методом, то дополнительные глаголы в пути избыточны:
| Метод | Плохо | Хорошо |
|---|---|---|
| POST | /users/create |
/users |
| DELETE | /deleteNote?id={id} |
/notes/{id} |
Коды ответов
Код позволяет клиенту принять решение без парсинга тела. Фронтенд на основе кода решает показывать ли данные, вывести ошибку валидации или отправить на страницу логина.
Первая цифра кода говорит о статусе действия и указывает, кто виноват. Все коды можно посмотреть на HTTP Cats, здесь приведу те, которые вы будете использовать каждый день.
| Код | Когда использовать |
|---|---|
| 200 | Успешный запрос с телом ответа |
| 201 | Создан новый ресурс |
| 202 | Задача принята в обработку |
| 204 | Успех без тела ответа, когда достаточно статуса |
| 304 | Данные не изменились |
| 400 | Ошибка в параметрах запроса |
| 401 | Пользователь не авторизован |
| 403 | Недостаточно прав |
| 404 | Ресурс не найден |
| 409 | Конфликт состояния или данных |
| 422 | Ошибка валидации |
| 429 | Превышен лимит запросов |
| 500 | Внутренняя ошибка сервера |
| 502 | Ошибка вышестоящего сервиса |
| 503 | Сервис временно недоступен |
| 504 | Таймаут вышестоящего сервиса |
Не пытайтесь использовать все HTTP-коды. Большинству API достаточно 10–15. Важнее использовать их последовательно, чем стремиться к максимальному соответствию RFC.
Ошибки
Код ошибки помогает клиенту принять решение, но его недостаточно для понимания и отображения проблемы. Следует использовать единый формат ошибок, который будет содержать как минимум текстовый код и детализацию проблемы. Например:
{
"code": "invalid_email",
"message": "..."
}
Текстовый код поможет различить конкретные проблемы в общем коде ошибки: код 400 показывает, что с запросом что-то не так, а текстовый код покажет, в чём именно проблема.
В сообщение ошибки не следует писать технические подробности, такие как сырые ошибки базы и трейсы. Это небезопасно и раскрывает лишние детали устройства бэкенда.
Пагинация
Пагинация — это техника разделения списка ресурсов для постраничной отдачи. Это полезно, когда в базе много данных, а фронтенд единовременно показывает только часть. Есть 2 популярных метода пагинации: оффсетная и курсорная.
Оффсетная
Для оффсетной пагинации клиент передаёт отступ от начала списка offset и количество элементов limit, которое он хочет получить. Иногда используют параметры page и pageSize, которые перекладывают расчёт оффсета на сервер. Особой разницы нет, используйте то, что лучше подходит вашему API.
Подходит когда размер списка дёшево считается и элементы не появляются слишком часто.
Курсорная
При использовании оффсетной пагинации пользователь может увидеть дубликаты между страницами, если ресурсы появляются часто, потому что в базе уже появилось столько записей, сколько запрашивается для страницы.
Эту проблему решает курсорная пагинация. Задумка в том, что бэкенд отдаёт пачку элементов и ссылку на следующую пачку (курсор). Даже если в базе появились новые элементы, курсор всё ещё указывает на продолжение.
Минус курсорной пагинации в том, что обычно она не позволяет сразу перейти к произвольной странице: до нужной позиции приходится последовательно пройти предыдущие страницы.