Эта статья предназначения для специалистов, уже начинающих работать с REST API. Наверняка вы уже знакомы с практиками разработки REST API, но некоторые советы и популярные приемы могут оказаться не столь хорошими … Предлагаю посмотреть, как можно улучшить практики для интерфейсов, замыкающихся на объекты реляционных моделей. Возможна статья будет не очень интересна поклонникам канонического и уже архаичного REST’а, но сможет порадовать эффективных практиков.
Определения
/users: – маршрут коллекции
/user: – маршрут объекта
Получить пользователя по номеру телефона, почты, СНИЛС и т.д.
Если хотите получить один объект по id или ошибку 404, сделайте так …
/users/{id}:
Если хотите получить один объект по прочим уникальным ключам или ошибку 404, сделайте так …
/user: … parameters: phone, email, snils, login, etc.
Если нужно получить сравнительно небольшой список по критериям выборки, то так …
/users: … parameters: …
Получить список постранично можно сделать так …
/users/page: … parameters: pageNum, pageSize, etc.
Если список очень большой и получение предполагается в несколько итераций – используйте дополнительные параметры …
/users: … parameters: lastId, limit, sort, fields.
Напоминание, используйте параметр fields для подрезки трафика, забирайте только нужные данные.
Не более одного параметра в маршруте
Если у вас не составной ключ, что для реляционной модели редкость, то так …
Плохо:
/orders/{orderId}/items/{itemId}:
Зачем вообще нужен orderId, если есть itemId!
Хорошо:
/orders/items/{itemId}:
Делайте дружественный API
Если к вам будут ходить разные системы и каждая система хочет работать со своим ключом объекта – сделайте дружественное API. Полиморфность идентификатора реализуется так:
/users/{id}:
parameters:
- name: id
  description: Идентификатор объекта
  in: path
  required: true
  schema:
    type: string
- name: idType
  description: Тип идентификатора
  in: query
  schema:
    type: string
    default: sys
    enum: [sys, …]Избегайте ложных вложенностей
Предположим, есть магазин, в котором покупатели оставляют заказы, сразу хочется сделать такой маршрут …
/customers/{id}/orders: – заказы покупателя.
На самом деле заказ является центральной сущностью, а клиент, организация — это её атрибуты.
Плохо:
/customers/{id}/orders:
/organizations/{id}/orders:
/contracts/{id}/orders:
Не маловато ли, может еще пяток порисовать? Может можно обойтись одним? Да и вложенность фейковая, однако.
Хорошо:
/orders: … parameters: customerId, organizationId, contractId.
Еще пример, представим, что вам надо описать отношение учитель – ученик. Как правило, связь двух равноправных сущностей бывает нагружена дополнительными атрибутами …
Плохо:
/teachers/{id}/students:
/students/{id}/teachers:
Один интерфейс правда лучше, чем два, и вложенность тут липовая, и объекты не те (нужны не учителя или студенты, а полный объект с дополнительными атрибутами), поэтому …
Хорошо:
/teacher-student-relations: … parameters: teacherId, studentId, fields.
Используйте вложенность только для специфических случаев
- Для доступа к вложенным коллекциям. 
- Для non-CRUD операций. 
- Для вынесенных операций, чтобы изолировать потребителя от остального API. Пример, установить лимит для пользователя … 
PUT /users/{id}/limit:  # да, свойство объекта это тоже вложенный ресурс
Отказывайтесь от объектных методов, задействуйте методы коллекций по максимуму
И наконец совет, который очень упростит пользование API с фронта и при взаимодействии систем. Используя его, вы предотвратите: бессмысленную и беспощадную бомбардировку серверов запросами, чудовищную перегрузку баз данных, порадуете разработчиков UI (им часто требуется: замена, добавление или удаление списка за одну операцию). В итоге, учитывая требования к быстродействию, нагрузке, снижению трафика и возможные потребности для разработки UI получим такую практику:
GET коллекции
Получайте объекты пачками. Операция GET /users/{id}: вовсе не обязательна, пользуйтесь GET /users: … parameters: id, etc.
DELETE коллекции
Принимайте на вход массив идентификаторов и удаляйте объекты пачками.
POST/PUT коллекции
Передавайте набор объектов для массовой вставки (обновления). Объектная версия операции вовсе не обязательна.
PATCH коллекции
Передавайте набор свойств объектов для массового обновления. Используйте частичное заполнение объекта, точнее: id объекта + обновляемый набор свойств.
Чтобы полноценно реализовать совет вам потребуется конструкция allOf.
Создайте base-объект только с набором предметных свойств, без required. Далее наследованием создайте:
- для POST: новый объект от base-объекта с секцией required. 
- для PUT: новый объект от post-объекта + id (для метода коллекции), для объектного метода используйте post-объект. 
- для PATCH: новый объект от base-объекта + id. 
- для GET - новый объект от base-объекта + технические свойства + обогащения. 
Можно сократить количество объектов на 2 (PUT, PATCH), если в базовый сразу добавить id.
Не используйте параметры в маршруте
Ранее я советовал использовать не более одного параметра в маршруте, но это совет для ортодоксов. Я советую не использовать параметры в path совсем. Почему?
- Параметр в query может быть массивом, что полезно для получения, выбранных пользователем объектов, за одно обращение. 
- Так как длина URL весьма небольшая, то в некоторых практиках для передачи больших массивов их помещают в requestBody. А поскольку эту секцию нельзя использовать в методе DELETE, то вообще отказываются от HTTP методов (имя метода указывается в теле запроса отдельным атрибутом (см. API Яндекса - красота)), что очень сильно приближает API к объектной модели. 
- Маршрут читается легче, и очень близок к объектным методам процедурных языков, сравните … 
GET /orders: … parameters: customerId 
и getOrders(customerId)
Используйте PUT прагматично
Сакральное отличие PUT от PATCH в том, что вы сами или по просьбе потребителя даете ему возможность разговаривать с вашей системой приблизительно так «Я не знаю, что там в объекте изменилось, поэтому могу только вернуть его целиком.». Во всех остальных случаях используйте PATCH и вот почему:
- Обновлять весь объект целиком это нетипично для систем, замыкающихся на базы данных, 99% UPDATE по смыслу эквиваленты PATCH. 
- При передаче всего объекта непонятно что обновляется, а если понимать надо, то придется предварительно поднять старый объект из БД. 
- Передача большого количества объектов может плохо сказаться на трафике. 
- Сложнее разрулить конфликты в многопользовательской среде, с PATCH их будет банально меньше. 
Использование PUT для замены коллекции целиком тоже не лучший вариант. Поскольку POST нужен всегда, то лучше иметь в контракте один метод POST с опцией replace, чем два метода.
Ну вот и все, если кому нужна помощь в проектировании контракта welcome.
Комментарии (12)
 - nin-jin01.12.2021 15:40-2- Можно ещё добавить: выдавайте данные в нормализованном виде. Так вы и себе жизнь упростите (не надо денормализовывать ответ), и фронтам (не надо его обратно нормализовывать). - А ещё у меня есть идея one-line языка запросов, удобного для дебага, поддерживающего сложные (но не слишком) фильтрации, сортировки и выборки по подграфам. Можем обсудить, если тема интересна. 
 - YuryB01.12.2021 16:38+2- Для вынесенных операций, чтобы изолировать потребителя от остального API. Пример, установить лимит для пользователя … - PATCH /users/{id}/limit:- урл используется только для идентификации ресурса, грубое нарушение rest - Хорошо: - /teacher-student-relations- чем хорошо? лично мне совершенно не понятно что вернёт этот запрос. Это будет набор айдишек как из базы или это будут разные айди или даже разные типы данных в зависимости от параметра. Ещё раз напоминаю, в rest "url" идентифицирует ресурс, что идентифицирует этот урл? - Создайте base-объект только с набором предметных - с этого места вообще какой-то сумбур, ничего не понял о чём вы пишете 
 - sgrogov02.12.2021 01:42- Очень холиварная тема. На стэковерфлоу полно топиков с похожими обсуждениями и никому пока не удалось придти к удовлетворяющему всех подходу. - Избегайте ложных вложенностей - Я думаю главное, что нужно держать в уме, когда дизайнишь АПИ - это что АПИ чаще всего не просто интерфейс для манипуляции документами в базе данных. АПИ всё-таки интерфейс твоего приложения. И нужно руководствоваться именно сценариями использования приложения, а не схемой документов и диаграммой зависимостей таблиц в БД. - Гитхаб использует вложенные зависимости, например `GET /repos/{owner}/{repo}/pulls`, а не `GET /pulls?owner={owner}&repo={repo}`. И делает это потому, что GitHub - не приложение для фильтрации всех в мире PR. - Поэтому, вложенности хоть и нежелательны, но вполне допустимы. - Если нужно получить сравнительно небольшой список по критериям выборки, то так … - /users: … parameters: …- Фильтрация списков опциональными параметрами вынуждает разработчика АПИ заботиться о наличии индексов по всем комбинациям возможных фильтров. Иначе простой запрос может чрезмерно загрузить базу данных. - Мне кажется здесь стоит упомянуть, что опциональные фильтры должны быть последним средством. - используйте параметр fields для подрезки трафика, забирайте только нужные данные - ещё лучше используйте custom media type headers. Это и трафик сократит и сделает возможным форматирование результата под специфичные сценарии использования. - Если конечно ваш АПИ не просто интерфейс к методам базы данных.  - nin-jin02.12.2021 01:50-1- У гитхаба, кстати, очень не удобный апи. Особенно весело, когда репозиторий перемещается между владельцами. 
  - lysenkoan Автор07.12.2021 23:11-1- В этой статье заявленная тема всё же "API для интерфейсов, замыкающихся на объекты реляционных моделей". Здесь все операции CRUD. Разумно проектировать сразу как универсальный интерфейс и для своих систем с UI и для сторонних с UI и для сторонних без UI. В противном случае это путь в несколько API и бесконечную доработку. 
  - lysenkoan Автор07.12.2021 23:12-1- В этой статье заявленная тема всё же "API для интерфейсов, замыкающихся на объекты реляционных моделей". Здесь все операции CRUD. Разумно проектировать сразу как универсальный интерфейс и для своих систем с UI и для сторонних с UI и для сторонних без UI. В противном случае это путь в несколько API и бесконечную доработку. 
 
 
           
 


Dogrtt
Ну, хз-хз... /user - сомнительно, по другим предложениям - в чём сакральный смысл упаковки всего в query params? И вообще, что за структура: - Так плохо, так хорошо. Так плохо, так хорошо. А где объяснение, почему Вы считаете, что так плохо, а по другому хорошо? Я не особо давно в профессии, но большинство приведённых примеров, с моей точки зрения, как минимум спорны!
lysenkoan Автор
Тогда это статья для вас, в ней как раз и вскрыты все недостатки и архаичность канонических практик.