Как стать автором
Обновить

Комментарии 18

POST /users/{userId}/books/{bookId}/create — добавить книгу пользователю


Пишем «create», думаем «добавить». Не говоря уже о том, что технически это все будет CRUD create операция создания записи о принадлежности книги пользователю, для которой не стоит использовать глагол согласно пункта 1.
GET /tickets/booked

Похоже на обычный фильтр, можно не создавать для этого отдельный роут:
GET /tickets?status=booked

Некоторые также не любят заворачивать ответы в структуры с полями status, success и т.д., предполагается, что у нас уже есть для этого HTTP-статусы.
Добавлю три копейки.

Представьте, что ваш API работает в отказоустойчивой среде с балансировкой, а клиентом для него является bash + curl

1. Не используйте xml для обычного REST API.
Формат json отлично парсится чем угодно (например, jq).
Формат xml — это боль.

2. Не используйте модную когда-то технику «HTTP 200 OK» + реальный код внутри тела ответа.
Это влияет на кеширование, усложняет мониторинг, делает невозможным простые подзапросы типа auth_request из location итд итп.

3. Если по каким-то причинам вы вынуждены форсированно пагинировать ответ — возвращайте в ответе параметры пагинации. Хоть в хедерах, хоть как. И, блин, не забывайте писать о дефолтной пагинации в доках (вот пример того, когда забыли).

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

Хороша как статья, так и комментарии к ней! Теперь у меня есть шаблон для заметок по REST API :)
1 Не используйте глаголы в URL *

2 Используйте глаголы в URL

А лучше просто определиться — использовать или нет. А то устроили тут "ромашку". В так называемом "REST API" любую бизнес-логику можно выразить в терминах CRUD, и нет необходимости лепить дополнительные "глаголы". А если вы не претендуете на реализацию "REST API", и делаете вольную интерпретацию RPC — делайте везде глаголы, и вообще всегда через POST, вы и так уже выбрали свой особый путь, нечего лицемерить и пытаться следовать принципам, которые вас не устраивают.


POST /wishlist/{userId}/{bookId}

Это что вообще должно означать? "Найти в вишлисте сущность и 'именем' {userId} (наверное это такой ID вишлиста), внутри "сущности" найти "что-то" с 'именем' {bookId}, и в этом "что-то" создать "нечто"? Именно так я бы прочитал этот запрос, если бы мне сказали что это "REST API". Структура URL-а больше похожа на кальку с хранения виш-листа в виде таблицы в базе данных.
Но мне почему-то кажется, что вы имели ввиду вот такой запрос:


POST /users/{userId}/wishlist
   body: {"bookId": "some_book_id"}

Который читается примерно как: "Найти юзера с 'именем' {userId} и с помощю его ресурса 'wishlist' создать новую 'запись виш-листа' (если предположить типичный кейс использования метода POST)".
А если заранее известно, что ID новой записи в виш-листе — это всегда bookId, то можно этот API реализовать вот так:


PUT /users/{userId}/wishlist/{bookId}

Но это несколько снижает простор для "манёвра" на стороне сервера — не получится поменять ID записи в виш-листе, на что-то другое.


4 Используйте один идентификатор ресурса
Плохо: GET /bookings/{bookingId}/travellers/{travellerId}
Хорошо: GET /bookings/travellers/{travellerId}

Я считаю что одно другому не замена. Первый вариант нужен для работы с одной конкретной бронью (получить листинг путешественников, добавить или удалить кого-то в бронь). Второй же вариант нужен для непосредственной работы с путешественниками без привязки к брони, т.е. это просто как "записная книжка", справочник всех людей, с которыми вы могли бы путешествовать.


400 Bad Request — клиент отправил неверный запрос, например, отсутствует обязательный параметр запроса.

Рекомендуете читать спеку на HTTP статусы, и при этом предлагаете использовать статус 400 не по назначению. Этот статус означает только то, что сервер в принципе не понял, что ему прислали. Например ожидается JSON (допустим клиент это сообщил через Content-Type), а на самом деле в тело засунули содержимое JPEG-а. А если же сервер смог понять формат данных в запросе, распарсил данные без ошибок, и сломался при валидации на соответствие бизнес-требованиям, то для этого есть другой статус — 422.


7 Модификаторы получения ресурса

Полностью согласен с коментарием выше — в ваших примерах было бы правильнее не плодить доп. сущности, а просто добавить фильтр (/tickets?status=booked)


8 Выберите одну структуру ответов

В ваших примерах структура совершенно не "одна", в рамках работы с одним и тем же ресурсом. И кроме того она предполагает, что клиент должен смотреть на status в теле ответа, а не в заголовке (иначе зачем его там дублировать?). Разработчики на языках со статической типизацией не очень любят, когда структура может сильно меняться в зависимости от значания одного из полей этой структуры. Например у вас для status == 0 поле data будет иметь одну структуру, а для 409 — очевидно другую. И вот это очень не удачное решение. В вашем примере будет правильнее если структура ответа может меняться только в зависимости от HTTP-статуса. Тогда разработчики клиентов, смогут реализовать простой matching HTTP-статуса на то, какая структура ожидается в теле ответа.


9 Все параметры и json в camelCase

А вот это прям совсем "вкусовщина", которую не стоит навязывать. Навязывать стоит только соблюдение единого стиля. Если выбрал camelCase — то везде делать в нём. Выбрал другой стиль — используй его, не смешивай с другими.


10 Пользуйтесь Content-Type

Ваш пример не понятно о чём. Изначально он предполагает возможность получить один и тот же ресурс в разных форматах (xml или json). Предложеное же вами решение вообще меняет ТЗ и исключает эту возможность (или вы забыли указать как её реализовать).
Если реально надо, что бы на одном URL-е можно было получать разные форматы ответа, то для этого клиент должен в своём запросе передавать заголовок Accept с нужным ему типом.
А указывать правильный Content-Type в запросах и ответах — это пракитически обязательное требование, которое лучше соблюдать не зависимо от того указано в URL-е "расширение файла" или нет.

В так называемом "REST API" любую бизнес-логику можно выразить в терминах CRUD, и нет необходимости лепить дополнительные "глаголы". А если вы не претендуете на реализацию "REST API", и делаете вольную интерпретацию RPC — делайте везде глаголы, и вообще всегда через POST, вы и так уже выбрали свой особый путь, нечего лицемерить и пытаться следовать принципам, которые вас не устраивают.

Обычно так называемый "REST API" это и есть RPC API ориентированный на CRUD. Термин REST тут применяется просто как вирусный баззворд. Кстати, я посмотрю как мы будем выкручиваться с CRUD для вызовов вроде login, withdraw, increment, close :) Особенно с учетом того, что сами HTTP методы не соответствуют набору CRUD операций.

Легко готов показать Вам как можно "выкрутится".


  1. login
    Что "физически" делает эта операция? Как правило она на основе входных параметров создаёт некий токен — специальный идентификатор, с помощью которого можно аутентифицировать последующие запросы. Или можно назвать это созданием сессии и использовать соответствующее имя ресурса в URL-е. С этого момента уже всё просто:


    POST /auth_tokens
    body: {"email": "...", "password": "..."}

    Если у вас в системе такие токены ещё и сохраняются в базу данных, то можно дополнительно реализовать API для работы с ними (листинг, удаление)


    GET /users/{user_id}/auth_tokens
    GET /users/{user_id}/auth_tokens/{token_id}
    DELETE /users/{user_id}/auth_tokens/{token_id}

  2. withdraw
    Списание денег со счёта, правильно я понимаю? Обычно, в правильных системах, такое делают через создание финансовой транзакции, или можно назвать её "операцией". Улавливаете шаблон?


    POST /users/{user_id}/transactions
    body: {'amount': -10.5}
    # Листинг истории транзакций
    GET /users/{user_id}/transactions
    GET /users/{user_id}/transactions/{trans_id}

    И я думаю, что редко используется чистое списание в вакууме. Обычно оно является частью более сложной логики: покупка товара, перевод денег на другой счёт и т.д. В каждом из этих случаев так или иначе есть процесс создания какой-то "операции", которая должна быть сохранена в базе для истории и отчётности.


  3. increment
    Физически это частичное изменение имеющегося ресурса. Следовательно метод PATCH. А дальше дело за тем, что бы придумать (или найти готовый) формат представления изменений, которые надо применить к ресурсу. Например можно позаимствовать "язык" обновлений из MongoDB для аналогичного кейса:


    PATCH /counters/{counter_id}
    body: {"$inc": {"value": 2}}

    А если требуется история этих изменений, то можно реализовать любые подобные изменения через их создание:


    POST /counters/{counter_id}/changes
    body: {"$inc": {"value": 2}}

  4. close
    Не совсем понятно из названия, что там именно "закрывается". Реализации могут быть разными в зависимости от контекста.
    Например через частичное изменение ресурса, в котором будет установлено поле closed в значение true. После чего сервер запрещает изменение ресурса.
    Или если "закрытый" ресурс может "исчезнуть" из системы — тогда подойдёт удаление "закрываемого" ресурса методом DELETE.
    Или опять таки через создание "операции", которая выполняет закрытие — это если нужна история работы с ресурсом или это действие не быстрое (настолько, что есть риск разрыва связи с клиентом, пока оно выполняется) и клиенту надо отслеживать прогресс выполнения операции.



HTTP методы не соответствуют набору CRUD операций.

Да, у некоторых HTTP-методов более широкий смысл нежели у их эквивалента в абривиатуре CRUD.
POST — это не "создание", это отправка данных на сервер. В контексте "REST API" наиболее частым вариантом использования этой информации — это создание нового ресурса, для которого URL не известен клиенту. Как правило, в этом случае предполагается, что запрос отправляют "контейнеру", внутри которого будет создан новый ресурс и "контейнер" сам сгенерирует для него ID.
PUT — полная замена ресурса по указаному URL-у. Заменить можно в том числе и "ничего" — это фактически создание нового ресурса.
PATCH — частичное изменение ресурса по указанному URL-у. Это вполне подходит к слову Update из CRUD. Также можно использовать для создания нового ресурса, если переданных в запросе данных для этого достаточно.
Ну и с GET, DELETE и так всё понятно.


"REST API" это и есть RPC API ориентированный на CRUD

Я не согласен с таким определением. "Базворд" REST — это не пустой звук, под ним понимаются вполне конкретные архитектурыне принципы построения распределённых клиент-серверных приложений. Один только CRUD не сможет реализовать эти принципы. И дисертация, описывающая REST, даже не говорит про CRUD (насколько помню). CRUD "нарисовался", скорее всего, из методов HTTP, которые можно узко использовать именно для этого.
Ну и как я уже упоминал — любую логику можно реализовать через CRUD, и часто такое решение будет удобнее и предоставлять больше возможностей, нежели "лёгкий" путь RPC ("что вижу о том и пою"). Выше я привёл реализацию withdraw, которая через CRUD сразу же даёт нам типовой "REST API" для просмотра истории "транзакций". О том как работать с таким API, может без документации догадаться любой, кто имел дело с нормальным "REST API".

Физически это частичное изменение имеющегося ресурса. Следовательно метод PATCH. А дальше дело за тем, что бы придумать (или найти готовый) формат представления изменений, которые надо применить к ресурсу.
PATCH /counters/{counter_id}
body: {"$inc": {"value": 2}}

Я же говорил, будете выкручиваться :) Ради того, чтобы не написать increment, мы вводим "служебное поле", которое не поле, а оператор, не являющийся частью обновляемых данных. Т. е. мы изобретаем микроязык запросов. Чтобы что?


Списание денег со счёта, правильно я понимаю? Обычно, в правильных системах, такое делают через создание финансовой транзакции, или можно назвать её "операцией". Улавливаете шаблон?

Это примеры правильных систем или нет?
https://developer.twitter.com/en/docs/api-reference-index.html
https://api.slack.com/methods
https://www.flickr.com/services/api/
https://vk.com/dev/methods
https://core.telegram.org/methods
https://developers.google.com/youtube/v3/docs/
https://www.dropbox.com/developers/documentation/http/documentation


Да, у некоторых HTTP-методов более широкий смысл нежели у их эквивалента в абривиатуре CRUD.

Из семи методов только два имеют семантический CRUD эквивалент. Создать ресурс может POST, PUT и PATCH. Получить может GET и POST (привет, ElasticSearch). Обновить может POST, PUT и PATCH. Удалить может POST и DELETE.


Я не согласен с таким определением. "Базворд" REST — это не пустой звук, под ним понимаются вполне конкретные архитектурыне принципы построения распределённых клиент-серверных приложений.

Какие именно архитектурные принципы и какое к ним отношение имеет CRUD? Приставка REST в большинстве постов про API это модный пустой звук. Её используют просто ради красного словца, пересказывая выдумки в интернете по принципу "сломанного телефона". Сейчас к почти любому HTTP API приписываeтся приставка REST/RESTful даже если автор понятия не имеет об обязательности гимермедиа в REST или идемпотентности PUT. Об правилах нейминга URI и CRUD — никогда не существовавших ни в REST, ни в HTTP — в мусорных туториалах пишут в таких огромных количествах, потому что об этом легко писать.


CRUD "нарисовался", скорее всего, из методов HTTP, которые можно узко использовать именно для этого.

Я думаю что CRUD нарисовался из его ошибочного сопоставления с HTTP методами и ошибочной интерпретации термина "resource" как документа. Это породило другой миф о глаголах в URI и анти-POST кампанию в своё время. Но если копнуть чуть глубже, окажется, что ни URI, ни HTTP никак не ограничивают сферу того, что можно отнести к "ресурсам", а REST определяет ресурс как семантику, а не на значение, которое соответствует этой семантике. Собственно, на определении ресурса из REST базируются определения ресурса в URI и HTTP.
https://www.ics.uci.edu/~fielding/pubs/dissertation/evaluation.htm#sec_6_2_1
https://tools.ietf.org/html/rfc3986#section-1.1
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-743


Ну и как я уже упоминал — любую логику можно реализовать через CRUD, и часто такое решение будет удобнее и предоставлять больше возможностей, нежели "лёгкий" путь RPC ("что вижу о том и пою").

Выше я привёл реализацию withdraw, которая через CRUD сразу же даёт нам типовой "REST API" для просмотра истории "транзакций".

  1. Почему низкоуровневый CRUD должен быть более удобным, чем API, который работает в терминах предметной области?
  2. Вы противопоставляете RPC с RPC. Пока на сервере есть набор конечных точек с собственной логикой, на которую жестко завязан клиент (например, ожидает, что по конкретной ссылке всегда возвращается объект товара), это будет взаимодействие в RPC-стиле. Не важно, CRUD или нет, это RPC. Здесь нет концептуальной разницы. Главне ограничение REST — HATEOAS — призвано разорвать эту связь, чтобы сервер сам переводил клиента в нужное состояние. На этом принципе работает весь гипертекстовый веб. Знаете, что означает REpresentaional State Transfer?

О том как работать с таким API, может без документации догадаться любой, кто имел дело с нормальным "REST API".

На примере $inc я увидел как "без документации" работать с таким API.

Т. е. мы изобретаем микроязык запросов. Чтобы что?

Что бы не изобретать микросистему наименования функций, которые будут решать такие типовые задачи?
Что бы было в системе единое решение для этих задач, а не кучка отдельных функций, каждая из которых решает свою задачу своим способом, иногда совсем не таким как другая, похожая на неё функция.
Сегодня вам нужна только функция инкремента одного поля, а завтра попросят добавить возведение в квадрат второго поля ресурса — и вот у вас уже 2 функции. Затем прилетит задача — "Делать одновременно инкремент первого поля и возведение в квадрат второго, атомарно". И вы делаете третью функцию. Через месяц добавляется третье и четвёртое поле и вы начинаете охреневать от числа возможных комбинаций. В конце концов вы напишете одну убер-функцию, которая принимает много параметров со специальными значениями, или ещё что-то придумаете — в общем изобретёте свой микро-язык для внесения изменений в ресурс.
PS: В моей работе пока не требовалось таких сложностей как инкременты. Хватает простого "языка" для замены значений указанных в JSON полей, не трогая тех, которые не указаны. Но насколько знаю где-то есть описание "языка" для PATCH запросов, который позволяет в том числе делать и инкременты. С ходу не могу нагуглить — давно видел, поэтому и предложил вариант языка из MongoDB, т.к. по сути он очень похож и решает ту же задачу.


Это примеры правильных систем или нет?

Под правильными, я имел ввиду общие принципы ведения бухгалтерии и работы с деньгами и счетами, а не какой-то конкретный API. В таких системах обычно не бывает простого "списать_деньги()". Если где-то что-то убывает, в другом месте должна быть прибыль. И все операции изменения счёта сохраняются — нельзя просто так взять и сделать "инкремент" значения счёта. Как делать инкремент мы уже разобрали — если бухгалтерские принципы не важны для приложения, то можно и таким образом "списывать" деньги со счёта.


Приставка REST в большинстве постов про API это модный пустой звук.

Полностью с Вами согласен по этому поводу. Я писал исключительно про то как это описано в первоисточнике.


Почему низкоуровневый CRUD должен быть более удобным, чем API, который работает в терминах предметной области?

Частично я уже про это написал — чем меньше вариативность возможных операций, которые можно выполнить надо какой то "сущностью" в системе, тем проще поддерживать и развивать такую систему. Проще её документировать и разбираться в ней новичкам. Легче реализовать различные инструменты для автоматизации и мониторинга.
Я был в своё время с "другой стороны" — писал недо-REST, с глаголами в URL-ах. С уникальными методами, которые делались в соответствии с "предметной областью", а не с тем как это на самом деле реализовано изнутри. И мне это сильно надоело бардаком, сложностью рефакторинга, трудностью документирования и отсутствием единых правил в работе API. И ещё гемороем с поддержкой сделанных "предметных" API, т.к. их внешний интерфейс совсем не согласовывался с особенностями реализации. Например снаружи есть "удалить_все_файлы()", а внутри просто не реально сделать такое, что бы оно выполнялось в разумное время. И часто про эту проблему узнаёшь слишком поздно, когда клиенты уже написаны и находятся в продакшене — приходится изворачиваться и лепить костыли.
И в итоге я пошёл в сторону "REST-просветления" и "ограничения свободы". Стало гораздо лучше. Сходу получилось реализовать генератор документации, все API устроены похожим образом и легко добавлять в них массово новые фичи (например поддержку заголовка ETag и условных HTTP-запросов).


Вы противопоставляете RPC с RPC.

В целом наверное да, всё что угодно можно назвать RPC, т.к. фактически всё сводится именно к этому — вызову какой-то функции. Но, согласитесь, под RPC обычно принято понимать систему с одной точкой входа (один URL). А какую "именно" функцию вызвать — передаётся через аргументы. И RPC не предполагает ни каких ограничений (ни стилевых, ни структурных). Называй функции как хочешь, передавай любые аргументы, делай внутри любую дичь.
А когда разработчик ставит себя в жёсткие рамки, ограничивает себя CRUD-ом, работает в концепции "всё есть ресурс", задумывается над тем какие HTTP-статусы он будет возвращать в тех или иных ситуациях и др. То начинают открываться новые возможности и видение проблем, которые могут случится, но про которые бы не задумался делая RPC в терминах предметной области (если будет интересно, то могу отдельно привести пример из реальной жизни про API "удалить_все_файлы()").


На примере $inc я увидел как "без документации" работать с таким API.

Я имел ввиду, что без документации просто догадаться, что если есть ресурс "контейнер", в который можно через POST "добавлять" другие ресурсы, то очень вероятно можно так же выполнить листинг этого контейнера, запросить один элемент из него, удалить или поменять этот элемент. Сразу ясно каким образом это может делаться. Для этого дока не нужна. А если есть HATEOAS, то даже ещё проще.
Дока понадобится уже ближе к делу, когда захочется узнать какие конкретно параметры можно передавать в упомянутых операциях.

Чувствую, на ваш комментарий я разрожусь лонгридом.


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

Хватает простого "языка" для замены значений указанных в JSON полей, не трогая тех, которые не указаны. Но насколько знаю где-то есть описание "языка" для PATCH запросов, который позволяет в том числе делать и инкременты.

Может, лучше просто взять GraphQL?


Под правильными, я имел ввиду общие принципы ведения бухгалтерии и работы с деньгами и счетами, а не какой-то конкретный API. В таких системах обычно не бывает простого "списать_деньги()".

https://developer.payoneer.com/docs/OpenAPI/APIDoc/api/payments/withdraw
https://developer.wepay.com/api/api-calls/checkout
https://www.liqpay.ua/documentation/api/p2p_credit/doc


Полностью с Вами согласен по этому поводу. Я писал исключительно про то как это описано в первоисточнике.

Так в первоисточнике нет ни слова о CRUD или нейминге URI, который не имеет никакого значения для архитектурного стиля.


Самое интересное то, что первоисточники строго противоречат вашим словам, вплоть до наоборот.

«There is no such thing as a REST endpoint. There are resources. A countably infinite set of resources bound only by restrictions on URL length. A client can POST to a REST service to create a resource that is a GraphQL query, and then GET that resource with all benefits of REST…»
Roy Fielding


«You won't find a constraint about "nouns" anywhere in my dissertation. It talks about resources, as in resources, because that is what we want from a distributed hypermedia system (the ability to reuse those information sources through the provision of links). Services that are merely end-points are allowed within that model, but they aren't very interesting because they only amount to one resource.»
Roy Fielding (RESTful representation of nouns?)


«A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server). Servers must have the freedom to control their own namespace. Instead, allow servers to instruct clients on how to construct appropriate URIs, such as is done in HTML forms and URI templates, by defining those instructions within media types and link relations. [Failure here implies that clients are assuming a resource structure due to out-of band information, such as a domain-specific standard, which is the data-oriented equivalent to RPC’s functional coupling]»
Roy Fielding (REST APIs must be hypertext-driven)


«At no time whatsoever do the server or client software need to know or understand the meaning of a URI — they merely act as a conduit through which the creator of a resource (a human naming authority) can associate representations with the semantics identified by the URI.»
Roy Fielding (Evaluation)


«You should not be building clients that are dependent on the resource naming structure. There is simply no need to do so — the hypertext sends the client directly to the desired application state.»
Roy Fielding (REST APIs must be hypertext-driven)


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

Вы не сделали вариативность меньше, вы спрятали вариативность в тело запроса. Интересно, как именно это облегчает мониторинг? Рейты RPS тоже удобнее делать? :)


Я был в своё время с "другой стороны" — писал недо-REST, с глаголами в URL-ах. С уникальными методами, которые делались в соответствии с "предметной областью", а не с тем как это на самом деле реализовано изнутри.

И стали писать недо-REST без глаголов в URL? :) Кстати, почему HTTP API, даже будучи реализованным в терминах CRUD, часто спрятан внутри языкового SDK который вместо CRUD-кишок раскрывает высокоуровневые вызовы?
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/pinpoint.html
https://elasticsearch-py.readthedocs.io/en/v7.13.0/api.html


И в итоге я пошёл в сторону "REST-просветления" и "ограничения свободы". Стало гораздо лучше.

Я ничего не понял.


Сходу получилось реализовать генератор документации, все API устроены похожим образом и легко добавлять в них массово новые фичи (например поддержку заголовка ETag и условных HTTP-запросов).

ETag в ответах добавляется легко и это никак не зависит от того, делаете вы GET /products.search?q=abc или GET /products?q=abc. Аналогично с генерацией OpenAPI.


В целом наверное да, всё что угодно можно назвать RPC, т.к. фактически всё сводится именно к этому — вызову какой-то функции.

Не совсем, принципиальная разница между RPC и REST состоит в Uniform interface.


Но, согласитесь, под RPC обычно принято понимать систему с одной точкой входа (один URL). А какую "именно" функцию вызвать — передаётся через аргументы. И RPC не предполагает ни каких ограничений (ни стилевых, ни структурных). Называй функции как хочешь, передавай любые аргументы, делай внутри любую дичь.

  1. Я понятия не имею, откуда вы это берёте. RPC может иметь одну точку входа, или несколько, или по одной на каждый вызов. Это не принципиально. В вашей модели GrapрQL это RPC или нет?
  2. RPC не предполагает никаких ограничений, это вы, как программист, их предполагаете. В PATCH с вашим кастомным DML тоже легко засовывается любая дичь, включая операторы с мутациями. И как мы это валидировать-то будем? А если админ может менять статус заказа, а модератор нет?

А когда разработчик ставит себя в жёсткие рамки, ограничивает себя CRUD-ом, работает в концепции "всё есть ресурс".

Все-таки, что такое "ресурс"?


То начинают открываться новые возможности и видение проблем, которые могут случится, но про которые бы не задумался делая RPC в терминах предметной области (если будет интересно, то могу отдельно привести пример из реальной жизни про API "удалить_все_файлы()").

Не существует никакой концептуальной разницы между POST /files.drop, POST /delete_all_files и DELETE /files. На большой выборке этот вызов может заглохнуть не зависимо от того, как вы его назвали — в терминах CRUD или в терминах предметной области.


Я имел ввиду, что без документации просто догадаться, что если есть ресурс "контейнер", в который можно через POST "добавлять" другие ресурсы, то очень вероятно можно так же выполнить листинг этого контейнера, запросить один элемент из него, удалить или поменять этот элемент. Сразу ясно каким образом это может делаться. Для этого дока не нужна. А если есть HATEOAS, то даже ещё проще.

Работа с API в реальности очень далека от игрушечных примеров вроде "получить листинг товаров", но вы это и так должны знать. Документация нужна всегда.

Может, лучше просто взять GraphQL?

Если в его языке есть инкременты — можно и его использовать. Я просто не очень с ним знаком, но хорошо знаю "язык" MongoDB.


Так в первоисточнике нет ни слова о CRUD или нейминге URI

Да, там есть только понятие resource identificator и пример его реализации для web-а — URL. Можно конечно "сову" натянуть так сильно, что и глаголы считать "идентификаторами" ресурсов, но я предпочитаю более строгий вариант — только существительные.


Кстати, почему HTTP API, даже будучи реализованным в терминах CRUD, часто спрятан внутри языкового SDK который вместо CRUD-кишок раскрывает высокоуровневые вызовы?

Наверное потому, что большинство языков программирования предоставляют только один удобный способ что-то делать — функции. Вызывать функции в них понятнее и привычнее, чем менять состояние "объекта" как-то по другому. Хотя в языках где есть "property" c возможностью написать для него "setter", можно было бы попробовать изобразить "REST-в-бутылке", но думаю это будет смотреться дико и чужеродно. Особенно если в языке нет исключений, через которые можно было бы хоть как-то вернуть ошибку в процессе изменения свойств "объекта".


ETag в ответах добавляется легко

Если HTTP API не работает в концепции "всё-есть-ресурс", то не понятно что этот ETag будет означать. Например в запросе:


POST /payments/{payment_id}/cancel

какой смысл будет иметь ETag для глагола "cancel", при выполнении условного запроса (If-Match)? Тут он имеет смысл только для payment_id. Но я что-то сомневаюсь, что в HTTP предполагается "перенос" ETag с одного "ресурса" на другой. Ни одна стандартная реализация системы клиентского кеширования не будет такое делать.
Сделать возврат ETag несложно. А вот сделать в одном месте проверку этого ETag для условных запросов (If-Match) уже зависит от реализации. Будет проблематично это сделать если обработчик HTTP-запроса это просто функция, а не какой-то стандартизированный объект ("ресурс"), у которого есть свойство etag. В таком случае придётся обработку условных запросов запихивать в каждую "функцию".


Все-таки, что такое "ресурс"?

Абстракция, которая предполагает, что у "ресурса" есть какие-то свойства, представление и т.п. Для "глагола" сложно вообразить какие у него могут быть свойства и представление. А без этого идут лесом часть возможностей заложенных в HTTP (например связанных с кешированием).


Не существует никакой концептуальной разницы между POST /files.drop, POST /delete_all_files и DELETE /files.

Если действовать в ограничениях "только CRUD" и "URL указывает на один ресурс", то не получиться сделать так как Вы написали. Если использовать DELETE — то можно удалить только один ресурс за раз. Но по условию надо удалить файлы из папки, а папку не трогать — поэтому не получится сделать DELETE /nodes/{dir_id}. Остальные ваши варианты запросов не подходят под заданные ограничения.
Остаётся вариант с реализацией ресурса "Удалятор", в котором, через метод POST, будут создаваться "задачи" по удалению всех файлов из папки (или не всех). И это уже концептуально разделяет процесс на два шага:


  • создание задачи — это очень быстрый запрос, который возвращает "задачу", а не подтверждение что файлы удалены;
  • выполнение задачи — этот шаг может реализовываться как угодно, в том числе в "фоновых воркерах", которые "слушают" очередь задач и выполняют их. Тут у нас в общем случае нет лимита на время работы, т.к. не надо срочно-срочно вернуть клиенту ответ, пока связь не разорвалась.

Клиент в это время может "пулить" задачу по её URL-у и узнавать прогресс выполнения.


И это будет правильное решение, если не считать вариант "пускай клиент удаляет по одному файлу". В моём случае этот решение было найдено благодаря тому, что я стал следовать жестким ограничениям, которые в "интернетах" принято считать свойствами "REST-API".
До этого этот API был реализован примерно как вы писали: POST /delete_all_files. И этот API всё ещё есть в бекенде для обратной совместимости, но внутри он обмазан костылями — запускает задачу, ждёт 20 секунд пока воркер её выполнит. Если воркер не успел — API возвращает клиенту ошибку, а воркер продолжает свою работу. По другому ни как, т.к. клиент, который использует такое API, ожидает либо ошибку, либо успешный ответ, который говорит о том, что все файлы были удалены.

Да, там есть только понятие resource identificator и пример его реализации для web-а — URL. Можно конечно "сову" натянуть так сильно, что и глаголы считать "идентификаторами" ресурсов, но я предпочитаю более строгий вариант — только существительные.

Не нужно ничего натягивать: This specification does not limit the scope of what might be a resource; rather, the term "resource" is used in a general sense for whatever might be identified by a URI. [...] This specification does not place any limits on the nature of a resource, the reasons why an application might seek to refer to a resource, or the kinds of systems that might use URIs for the sake of identifying resources. [...] Likewise, the "one" resource identified might not be singular in nature (e.g., a resource might be a named set or a mapping that varies over time).
https://greenbytes.de/tech/webdav/rfc3986.html#overview


Если HTTP API не работает в концепции "всё-есть-ресурс", то не понятно что этот ETag будет означать. Например в запросе:
POST /payments/{payment_id}/cancel
какой смысл будет иметь ETag для глагола "cancel", при выполнении условного запроса (If-Match)?

Он не будет иметь никакого смысла для cancel, и я не очень представляю, зачем вообще передавать ETag на такие запросы. ETag в основном используется для кеширования при GET-запросах, и там нейминг значения не имеет.


GET /products.search?brand=cien

HTTP/1.1 200 Ok
ETag: W/"123"

GET /products.search?brand=cien
If-None-Match: W/"123"

HTTP/1.1 304 Not Modified

Будет проблематично это сделать если обработчик HTTP-запроса это просто функция, а не какой-то стандартизированный объект ("ресурс"), у которого есть свойство etag. В таком случае придётся обработку условных запросов запихивать в каждую "функцию".

Обработчик HTTP-запроса это всегда функция, а ETag присваивается результату вызова этой функции. Он может быть частью "хранимого ресурса", а может вычисляться на лету, это не важно.


Абстракция, которая предполагает, что у "ресурса" есть какие-то свойства, представление и т.п. Для "глагола" сложно вообразить какие у него могут быть свойства и представление. А без этого идут лесом часть возможностей заложенных в HTTP (например связанных с кешированием).

Какие именно свойства? Почему "представление" не может представлять результат обращения к ресурсу, или представлять состояние, в которое должен перейти клиент?
С точки зрения HTTP, вполне может: an abstraction is needed to represent ("take the place of") the current or desired state of that thing in our communications. That abstraction is called a representation.


Если действовать в ограничениях "только CRUD" и "URL указывает на один ресурс", то не получиться сделать так как Вы написали. Если использовать DELETE — то можно удалить только один ресурс за раз.

Разве DELETE /users/1 не удалит и корзину пользователя? Почему в вашей модели ресурс не может быть коллекцией? Почему мы не можем применить DELETE к коллекции? Можем, сработает каскадное удаление. В WebDAV, применив DELETE к директории, мы удаляем все поддерево. И не только в WebDAV. Это не противоречит ни HTTP, ни CRUD, ни REST.


In effect, this method is similar to the rm command in UNIX: it expresses a deletion operation on the URI mapping of the origin server rather than an expectation that the previously associated information be deleted.
https://greenbytes.de/tech/webdav/rfc7231.html#DELETE


Resources are not storage items (or, at least, they aren’t always equivalent to some storage item on the back-end). […] Likewise, a single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items.
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-743


Наверное потому, что большинство языков программирования предоставляют только один удобный способ что-то делать — функции. Вызывать функции в них понятнее и привычнее, чем менять состояние "объекта" как-то по другому. Хотя в языках где есть "property" c возможностью написать для него "setter", можно было бы попробовать изобразить "REST-в-бутылке", но думаю это будет смотреться дико и чужеродно.

ORM так и работают. Нет технических препятствий чтобы это сделать.


И это будет правильное решение, если не считать вариант "пускай клиент удаляет по одному файлу". В моём случае этот решение было найдено благодаря тому, что я стал следовать жестким ограничениям, которые в "интернетах" принято считать свойствами "REST-API".

Выполнение тяжелых задач в отдельном спулере это самоочевидное решение, для этого не обязательно читать мусорные посты о CRUD в интернетах. Когда создается заказ или меняется его статус, мы должны отправить пользователю email с pdf, и этот процесс может занять 3 — 10 секунд зависимо от размера заказа и настроения нашего email-провайдера. Других решений, кроме как создавать джобу в очереди, возникнуть не может.

Не нужно ничего натягивать

Всё что там написано, по моему, означает что "ресурс" — это не обязательно что-то "реальное" (типа документа или файла). "Ресурс" может быть эфемерной сущностью, например "удалятор". Но есть важная характеристика — это всё таки "сущность", а не "действие" (глагол). "Ресурс" можно адресовать, на него можно "указать пальцем". Поэтому я и написал про натягивание, т.к. "указывать пальцем" в основном принято с помощью существительных, а не глаголов. Криво смотрится инструкция: "создать 'купить' для пользователя 123".


ETag в основном используется для кеширования при GET-запросах

Есть очень даже полезное использование ETag в условных запросах отличных от GET. Например: "частично изменить ресурс, но только если его состояние не изменилось относительно имеющегося на клиенте". Делается это с помощью PATCH запроса + If-Match заголовка (или других условных заголовков). Если у клиента информация устарела, то такой запрос вернёт статус 412 и изменения не будут применены.


Обработчик HTTP-запроса это всегда функция, а ETag присваивается результату вызова этой функции.

Про то что несложно вернуть ETag в ответе на запрос, я согласен. Но я писал про другую сложность. Сложность с реализацией проверки ETag в условных запросах. Это надо делать ещё до того как будет вызван основной код обработчика. И в зависимости от результата проверки, основной код будет проигнорирован или выполнен. Как "фреймворк" получит ETag в этом случае? Если нет единой системы в которой "всё-ресурс", то придётся в каждую функцию-обработчик самому вставлять обработку условных запросов. А вот когда всё вертится вокруг "ресурсов", то можно реализовать единый интерфейс для них, который будет возвращать ETag сущности фреймворку. И последний сможет сам выполнить проверку и принять решение о следующих действиях.


С точки зрения HTTP, вполне может: an abstraction is needed to represent ("take the place of") the current or desired state of that thing in our communications.

В моём консервативном сознании не получается идентифицировать "thing" с помощью глагола. Если какое-то действие нельзя сделать с одним ресурсом через CRUD, то я просто сделаю другой ресурс, который будет это делать. Если мне надо "удалить всё", то я сделаю "удалятор". По количеству буковок в коде и документации это будет или так же, или незначительно больше, чем делать ресурс-глагол "удалить_всё". Но при этом у меня всё будет в рамках единой системы, которая позволит работать с этим ресурсом так же как с другими. Это не будет для системы каким-то "чёрным ящиком" в виде функции.


Разве DELETE /users/1 не удалит и корзину пользователя? Почему в вашей модели ресурс не может быть коллекцией? Почему мы не можем применить DELETE к коллекции? Можем, сработает каскадное удаление. В WebDAV, применив DELETE к директории, мы удаляем все поддерево.

Удалить "коллекцию" и "удалить содержимое коллекции" — это ведь существенно разные действия. Удаляя ресурс-коллекцию я могу в базе быстро пометить его удалённым и запустить фоновую задачу по удалению дочерних элементов. Все последующие запросы к дочерним элементам коллекции смогут проверить саму коллекцию и вернуть 404 не доходя до этапа получения элемента. Элемент даже может ещё быть в базе как "живой", если до него не дошла очередь в длительном процессе удаления в фоновом воркере. И нет проблем с листингом коллекции — его просто не надо делать.
А вот "удалить_все_файлы_в_папке" даже WebDAV не умеет. Там только один вариант — удалять файлы по одному. Пока жив ресурс-коллекция у нас нет ни каких гарантий, что она пустая. Вдруг между запросом "удалить_все_файлы" и "получить_листинг_коллекции" был выполнен запрос "добавить_в_коллекцию".


ORM так и работают. Нет технических препятствий чтобы это сделать.

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


Выполнение тяжелых задач в отдельном спулере это самоочевидное решение, для этого не обязательно читать мусорные посты о CRUD в интернетах.

Это очевидно после того как уже "лоб разбил", и начинаешь "соломку подкладывать". А поначалу об этом не задумываешься. В ТЗ написано "удалить все файлы в папке" — так и лепишь. Что может пойти не так? Это простой код — цикл по всем файлам, или один DELETE запрос в SQL базу. И даже не надо email отправлять или PDF генерить. В тестах всё работает, даже 1млн файлов в тестах удалится без проблем.
А в продакшене окажется, что и сеть не 100% надёжная, и таймауты на обработку запроса есть в клиенте и промежуточных HTTP-агентах. В лучшем случае это обнаружится в самом начале. Но скорее всего сильно поздно — когда в продакшене куча юзеров и у них накопилось много файлов.
А как "соломку подкладывать" в RPC варианте? Мне кажется, что в задаче "удалить_все_файлы", получится всё то же самое, что я сделал через ресурс "удалятор". Только это бует выражено "функциями":


  • "создать_задачу_для_удаления() -> task_id"
  • "получить_прогресс_выполнения_задачи(task_id)"

Т.е. по факту получится CRUD через функции "чёрные ящики" вместо "ресурсов".

Всё что там написано, по моему, означает что "ресурс" — это не обязательно что-то "реальное" (типа документа или файла). "Ресурс" может быть эфемерной сущностью, например "удалятор". Но есть важная характеристика — это всё таки "сущность", а не "действие" (глагол).

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


Our use of the terms "identify" and "identifying" refer to this purpose of distinguishing one resource from all other resources, regardless of how that purpose is accomplished (e.g., by name, address, or context). These terms should not be mistaken as an assumption that an identifier defines or embodies the identity of what is referenced, though that may be the case for some identifiers


Familiar examples include an electronic document, an image, a source of information with a consistent purpose (e.g., "today's weather report for Los Angeles"), a service (e.g., an HTTP-to-SMS gateway), and a collection of other resources. [..] Likewise, abstract concepts can be resources, such as the operators and operands of a mathematical equation, the types of a relationship


The target of an HTTP request is called a “resource”. HTTP does not limit the nature of a resource; it merely defines an interface that might be used to interact with resources.


Оператор, функцию или сервис тоже можно адресовать. Ресурсом может быть все что годно, что можно идентифицировать с помощью URI. Я не понимаю, о чем мы спорим.


Есть очень даже полезное использование ETag в условных запросах отличных от GET. Например: "частично изменить ресурс, но только если его состояние не изменилось относительно имеющегося на клиенте".

Я могу согласиться что условные HTTP-запросы для небезопасных методов мы теряем, но они не единственный способ сделать это. Эта защита реализуется тривиально, а для идемпотентных запросов предыдущие модификации обычно не имеют значения.


В моём консервативном сознании не получается идентифицировать "thing" с помощью глагола. Если какое-то действие нельзя сделать с одним ресурсом через CRUD, то я просто сделаю другой ресурс, который будет это делать.

У HTTP есть метод POST, семантика которого определяется семантикой целевого ресурса (The POST method requests that the target resource process the representation enclosed in the request according to the resource's own specific semantics). У "существительного" может быть своя собственная семантика?

У "существительного" может быть своя собственная семантика?

На "существительное" можно "указать пальцем". Ему можно что-то отправить (POST). Про него можно нормально говорить и писать человеческим языком без оказий, когда в качестве подлежащего или дополнения в предложении придётся использовать "глагол" (отправить число в купить). "Существительное" описывает то, что выполняет действия.
В общем меня лично коробит использовать глаголы с глаголами (HTTP-методы). И называть глаголами сущности в коде, которые обрабатывают HTTP-методы. Для меня использование глаголов в идентификаторах ресурсов — это натягивание совы, т.к. можно замечательно обойтись без них. И иметь в результате достаточно строгую систему с чёткими правилами. Правила, в которых есть понимание где "сущность" (ресурс), а где "действие" выполняемое с ним (HTTP-метод). В отличии от случаев, когда в одном месте в идентификаторе используют глаголы, а в другом почему-то существительные.
Кому-то это может показаться снижением гибкости. А я вижу в этом упрощение сложных систем и облегчение их поддержки.

На "существительное" можно "указать пальцем". Ему можно что-то отправить (POST). Про него можно нормально говорить и писать человеческим языком без оказий, когда в качестве подлежащего или дополнения в предложении придётся использовать "глагол" (отправить число в купить). "Существительное" описывает то, что выполняет действия.

Это уже пространные философские измышления


Для меня использование глаголов в идентификаторах ресурсов — это натягивание совы, т.к. можно замечательно обойтись без них. И иметь в результате достаточно строгую систему с чёткими правилами. Правила, в которых есть понимание где "сущность" (ресурс), а где "действие" выполняемое с ним (HTTP-метод).

Для меня натягивание бизнес-логики на CRUD-шаблон тоже выглядит как натягивание совы на глобус, особенно там, где такое натягивание ошибочно списывается на требования якобы HTTP или REST.


То, что вы делаете, называется Resource Oriented Architecture (https://google.aip.dev/121), но даже там Google вполне допускает использование т. н. "custom methods" (https://google.aip.dev/136) — действий, адресуемых URI и выполняемых через POST или GET. Такой дизайн API имеет все права на жизнь, но мы должны понимать, что это все еще ориентированный на данные RPC. Его использование никак не вытекает из REST или HTTP. Вдобавок Филдинг говорил, что "ROA is not REST. ROA is supposed to be a kind of design method for RESTful services, apparently, but most folks who use the term are talking about REST without the hypertext constraint. In other words, not RESTful at all."

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации