Эта статья предназначения для специалистов, уже начинающих работать с 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.

Используйте вложенность только для специфических случаев

  1. Для доступа к вложенным коллекциям.

  2. Для non-CRUD операций.

  3. Для вынесенных операций, чтобы изолировать потребителя от остального 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 совсем. Почему?

  1. Параметр в query может быть массивом, что полезно для получения, выбранных пользователем объектов, за одно обращение.

  2. Так как длина URL весьма небольшая, то в некоторых практиках для передачи больших массивов их помещают в requestBody. А поскольку эту секцию нельзя использовать в методе DELETE, то вообще отказываются от HTTP методов (имя метода указывается в теле запроса отдельным атрибутом (см. API Яндекса - красота)), что очень сильно приближает API к объектной модели.

  3. Маршрут читается легче, и очень близок к объектным методам процедурных языков, сравните …

GET /orders: … parameters: customerId

и getOrders(customerId)

Используйте PUT прагматично

Сакральное отличие PUT от PATCH в том, что вы сами или по просьбе потребителя даете ему возможность разговаривать с вашей системой приблизительно так «Я не знаю, что там в объекте изменилось, поэтому могу только вернуть его целиком.». Во всех остальных случаях используйте PATCH и вот почему:

  1. Обновлять весь объект целиком это нетипично для систем, замыкающихся на базы данных, 99% UPDATE по смыслу эквиваленты PATCH.

  2. При передаче всего объекта непонятно что обновляется, а если понимать надо, то придется предварительно поднять старый объект из БД.

  3. Передача большого количества объектов может плохо сказаться на трафике.

  4. Сложнее разрулить конфликты в многопользовательской среде, с PATCH их будет банально меньше.

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

Ну вот и все, если кому нужна помощь в проектировании контракта welcome.

Комментарии (12)


  1. Dogrtt
    01.12.2021 14:41
    +13

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


    1. lysenkoan Автор
      08.12.2021 00:29
      -2

      Тогда это статья для вас, в ней как раз и вскрыты все недостатки и архаичность канонических практик.


  1. nin-jin
    01.12.2021 15:40
    -2

    Можно ещё добавить: выдавайте данные в нормализованном виде. Так вы и себе жизнь упростите (не надо денормализовывать ответ), и фронтам (не надо его обратно нормализовывать).

    А ещё у меня есть идея one-line языка запросов, удобного для дебага, поддерживающего сложные (но не слишком) фильтрации, сортировки и выборки по подграфам. Можем обсудить, если тема интересна.


    1. alex-khv
      01.12.2021 16:11

      GraphSQL, не ?


      1. nin-jin
        01.12.2021 17:30
        -2

        Тут сравнение. А тут вкратце, что не так с GQL.


  1. YuryB
    01.12.2021 16:38
    +2

    Для вынесенных операций, чтобы изолировать потребителя от остального API. Пример, установить лимит для пользователя …

    PATCH /users/{id}/limit:

    урл используется только для идентификации ресурса, грубое нарушение rest

    Хорошо:

    /teacher-student-relations

    чем хорошо? лично мне совершенно не понятно что вернёт этот запрос. Это будет набор айдишек как из базы или это будут разные айди или даже разные типы данных в зависимости от параметра. Ещё раз напоминаю, в rest "url" идентифицирует ресурс, что идентифицирует этот урл?

    Создайте base-объект только с набором предметных

    с этого места вообще какой-то сумбур, ничего не понял о чём вы пишете


  1. superD
    01.12.2021 18:28

    Еще одна практика разработки REST API

    ...и еще один велосипед.

    Посмотрите спецификацию JSON API. Все уже детально описали и имплементировали на большое кол-во языков и платформ.


  1. lair
    01.12.2021 23:53

    Я так понимаю, в вопросах "плохо/хорошо" вам предлагается верить на слово?


  1. sgrogov
    02.12.2021 01:42

    Очень холиварная тема. На стэковерфлоу полно топиков с похожими обсуждениями и никому пока не удалось придти к удовлетворяющему всех подходу.

    Избегайте ложных вложенностей

    Я думаю главное, что нужно держать в уме, когда дизайнишь АПИ - это что АПИ чаще всего не просто интерфейс для манипуляции документами в базе данных. АПИ всё-таки интерфейс твоего приложения. И нужно руководствоваться именно сценариями использования приложения, а не схемой документов и диаграммой зависимостей таблиц в БД.

    Гитхаб использует вложенные зависимости, например `GET /repos/{owner}/{repo}/pulls`, а не `GET /pulls?owner={owner}&repo={repo}`. И делает это потому, что GitHub - не приложение для фильтрации всех в мире PR.

    Поэтому, вложенности хоть и нежелательны, но вполне допустимы.

    Если нужно получить сравнительно небольшой список по критериям выборки, то так …

    /users: … parameters: …

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

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

    используйте параметр fields для подрезки трафика, забирайте только нужные данные

    ещё лучше используйте custom media type headers. Это и трафик сократит и сделает возможным форматирование результата под специфичные сценарии использования.

    Если конечно ваш АПИ не просто интерфейс к методам базы данных.


    1. nin-jin
      02.12.2021 01:50
      -1

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


    1. lysenkoan Автор
      07.12.2021 23:11
      -1

      В этой статье заявленная тема всё же "API для интерфейсов, замыкающихся на объекты реляционных моделей". Здесь все операции CRUD. Разумно проектировать сразу как универсальный интерфейс и для своих систем с UI и для сторонних с UI и для сторонних без UI. В противном случае это путь в несколько API и бесконечную доработку.


    1. lysenkoan Автор
      07.12.2021 23:12
      -1

      В этой статье заявленная тема всё же "API для интерфейсов, замыкающихся на объекты реляционных моделей". Здесь все операции CRUD. Разумно проектировать сразу как универсальный интерфейс и для своих систем с UI и для сторонних с UI и для сторонних без UI. В противном случае это путь в несколько API и бесконечную доработку.