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 Ресурс не найден

Чтение заголовков ресурса. Похож на 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.

Подходит когда размер списка дёшево считается и элементы не появляются слишком часто.

Курсорная

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

Эту проблему решает курсорная пагинация. Задумка в том, что бэкенд отдаёт пачку элементов и ссылку на следующую пачку (курсор). Даже если в базе появились новые элементы, курсор всё ещё указывает на продолжение.

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