Предупреждение: не сомневаюсь, что наверняка кто-то, прочитав данную статью, возжелает закидать меня тапками, но это его право, я не претендую на истину.

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

В наше время каждый слышал про Web-API и у многих это ассоциируется с REST. Однако этот многострадальный REST всегда какой-то не такой, с ним все время что-то не так.

Разработчики часто спорят и упрекают друг друга, что твой REST недостаточно REST-овый. Это стало чем-то вроде культа или даже религии. Я не планирую рассказывать о том, как правильно готовить REST, но весь материал именно вокруг этого многострадального RESTа.

Вспомним, какие основные ограничения имеет REST:

  1. Модель клиент-сервер

  2. Отсутствие состояния (сервер между запросами клиента не хранит ничего промежуточного, другими словами, для выполнения запроса Б, серверу не нужны данные из запроса А)

  3. Кэширование

  4. Единообразие интерфейса

  5. Слои (клиент не знает, взаимодействует он с конечной системой напрямую или на пути есть какие-то другие сервисы)

Стоит сразу отметить, что REST это ни разу не про HTTP (для кого-то может быть сюрпризом).

Что ты такое несешь? Филдинг разрабатывал REST именно для HTTP!
Да, но REST - это архитектурный стиль, который может работать поверх других протоколов. HTTP - лишь наиболее популярная реализация.

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

Есть множество статей в интернетах, про то, как нужно его (REST) готовить и в статьях всегда всё красиво и понятно, с примерчиками (парочкой). Но эти примерчики часто оторваны от реальности, они элементарные и всегда заточены под одно и то же – под CRUD-операции (на всякий случай: CRUD — это create, read, update, delete). Из этих операций вытекают HTTP-методы: GET, POST, PUT/PATCH, DELETE. Причем, судя по тем реализациям, которые мне приходилось встречать, понимание методов PUT и PATCH разнится. Эти CRUD операции прекрасно укладываются в концепцию для работы с базами данных, со справочниками – создать/удалить пользователя/товар/услугу, получить данные о пользователе и т.д. и именно такие примеры приводятся в статьях. Но когда речь заходит об операциях – запустить/остановить/отменить (подчеркну: не удалить, а отменить, то есть прервать или сторнировать) операцию, тут возникает сложность.

Сложность возникает как минимум по двум причинам:

  1. из-за процедурного мышления (и здесь нет ничего плохого)

  2. из-за попытки притянуть за уши то, что не очень хорошо притягивается (переложить некоторые операции на ресурсы и домены)

Хочется спросить: а нужно ли; оправданно ли?

Ради чего стремиться к призрачному REST, если в какой-то ситуации он не очень-то и подходит? Но на это упорно не хотят обращать внимание. Кто сказал, что API по HTTP – это обязательно REST? Из моих наблюдений вырисовывается примерно такая картина, что, в основном, везде используется RPC (удаленный вызов процедур), который пытаются маскировать под REST, используя какие-то псевдо-домены с недоресурсами и впихивая всюду DELETE, PATCH и вот это вот всё. Что мы получаем в результате таких попыток – только боль. Это и не REST (в том виде, о котором говорил Филдинг) и не RPC, а какая-то ерунда. REST задумывался так, чтобы перенести глаголы из URI в HTTP-методы, а часть параметров из тела в URI. Разумеется, это удобно, когда можно обратиться к одному и тому же URI, но с разными HTTP-методами и результат будет разным. Условно, как если бы мы работали с обычными папками в проводнике. Но работа с файлами это не только создание/чтение/изменение/удаление. Есть ведь еще перемещение, копирование и выполнение (запуск). С последними тремя как раз возникают разногласия при перекладывании их на REST.

Недавно я встретил несколько кричащих примеров гибрида, когда RPC покусал RESTа, они выглядели примерно так:

  • DELETE /cat/delete-cat

  • PATCH /inv/delete-any

  • DELETE /foo/delete-foo?param1=value1

  • DELETE /res/delete-some/{id}

  • PUT /res/modify-some

В первых двух параметры удаляемого объекта передавались в теле, но и другие не далеко ушли. Кто-то скажет: «Ха! Ну уж я бы точно так не сделал!» Не зарекайтесь.

Что мы видим из примера выше? – классический RPC. JSON-RPC, если хотите (но не тот, который 2.0). Ну и отлично! Что в этом плохого? Мне лишь непонятно желание слепо натягивать ассорти HTTP-методов на RPC. Зачем? Это попытка таким образом сделать REST? Будто бы этот REST определяется именно использованием определенных HTTP-методов. Три полоски — это ведь еще не Адидас, не так ли? И вообще, мне непонятно, почему появилось это странное стремление делать REST и только его? Ну зачем притягивать за уши или натягивать то, что не очень хорошо натягивается? Какая-то повальная мания. А что вообще такое, этот REST?  Давайте вновь посмотрим на список ограничений REST (который приведен выше) и зададим другой вопрос – укладывается ли RPC по HTTP в эти ограничения, если сделать первый вызов с использованием другого HTTP-метода, например, POST /cat/delete-cat? Какому из пунктов ограничений это противоречит?

Да, есть семантика HTTP-методов (GET, POST и т.д.), о которых говорится в RFC7231, но без фанатизма и отрыва от реальности. Примеры выше как раз демонстрируют и то, и другое.

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

Ок, допустим, вы считаете, что характерным признаком REST являются как раз эти ваши GET, POST, PUT, так в педивикии написано и всё такое. А еще кто-то скажет, что типа эти HTTP-методы помогают понять, какая операция выполняется над ресурсом и предсказать другие операции, ну вот и предскажите, что еще может выполнять PATCH /inv/delete-any? И почему вы считаете глагол в URI менее информативным?

Если вы так любите REST, где же тогда так называемая «обнаруживаемость» (discoverability, HATEOAS), про которую в концепции REST тоже говорится – это когда вызов GET /university/123 вернет в теле не только информацию об учреждении, но и ссылки на все доступные нижестоящие ресурсы

/university/123/teachers
/university/123/students

Мне такие реализации (с HATEOAS) встречались крайне редко, ~0.5%.

Или вы считаете это необязательно? Это не такая существенная характеристика, ведь куда важнее всякие DELETE и PUT, правда? Вот они-то уж точно про REST! Так сказать, малой кровью, но уже почти RESTful.

Тема холиварная, поэтому, может быть, к этому моменту у кого-то возникла ко мне ненависть и желание заявить, что я вообще ничего не понимаю. Пусть так.

Однако некто Леонард Ричардсон, автор книги «RESTful Web APIs» и ряда других на эту тему утверждает, что пока у вас нет HATEOAS, ваша модель API не является RESTful.

Но вернемся к примеру выше: DELETE /cat/delete-cat

Этот метод явно не подразумевает какого-нибудь GET /cat/delete-cat в классическом представлении, даже если бы «delete-cat» в этом случае было не императивом, а неким именем сущности. Иначе он бы выглядел типа DELETE /cat/delete-cat/{catId}.

Поэтому я бы предложил следующее:

Если конечная точка (endpoint) не подразумевает использования всего набора HTTP-методов (GET, POST и т.д.), тогда не нужно к нему лепить отдельно стоящий метод в вакууме. Делайте в этом случае обычный RPC. Проанализируйте, сколько в вашем существующем или проектируемом API конечных точек, к которым применяется или планируется применять весь REST-овый набор HTTP-методов. Если к конечной точке применяется один или два HTTP-метода, лучше пересмотреть вызовы этих конечных точек в сторону RPC и использования одного HTTP-метода.

И не говорите, что POST это только про создание нового ресурса.

Вот цитата из RFC7231 п.4.3.3.

Метод POST и для чего он нужен:

- Providing a block of data, such as the fields entered into an HTML form, to a data-handling process;
- Posting a message to a bulletin board, newsgroup, mailing list, blog, or similar group of articles;
- Creating a new resource that has yet to be identified by the origin server;
- Appending data to a resource's existing representation(s).

Первый пункт как раз подходит под RPC.

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

Есть у нас обычная электрическая лампочка. Мы будем управлять лампочкой через API. У нее, по сути, только 2 состояния – вкл. и выкл. Сразу подчеркну: лампочка 1шт. Больше не предвидится.

Далее все по канонам: методом GET мы будем получать состояние; методом PUT мы будем менять состояние (записывать в порт пару байт). Пока что все сходится? Можем ли мы здесь применить POST и DELETE? Лампочка – это физический ресурс, мы не можем ее удалить из патрона, мы не можем вкрутить новую, мы не можем добавить еще одну лампочку к существующей. Какой бы вы прикрутили сюда endpoint чтобы получилось REST-фульно, чтобы не стыдно было перед коллегами и чтобы масштабируемость (если понадобится), и всё такое прочее?

Я бы здесь предложил RPC. С использованием GET и POST, а может быть даже только одного POST и для чтения, и для записи, про GET чуть позже.

POST /lamp/123/commands
{
"action": "turn_on"
}

POST /lamp/123/commands  
{
"action": "turn_off"
}

Но ситуация меняется, если бы, например, у нас был какой-нибудь Modbus на нескольких RS-485 и с N контроллеров на каждой шине, тогда POST и DELETE можно было бы использовать для добавления новых устройств, их адресов и регистров... но куда? В некое хранилище/БД. Снова все сводится к хранилищу и вот они наши CRUD-операции нарисовались.

И маршрут был бы примерно такой:

Добавляем устройство в контроллер
POST /ports/1/controllers/240/devices

А теперь нужно вкл/выкл лампочку
/ports/1/controllers/240/devices/1

Это будет POST, PUT или PATCH? А DELETE здесь уместен? Вопрос риторический.

Однако если посмотреть на это через призму большинства реализаций и переконвертировать, получится нечто подобное:
PUT /lamp/enable
GET /lamp/info?id=123

Но это же не REST, это RPC! Почему такая реализация? Потому что процедурное мышление. А еще, потому что метод и маршрут появился не на заре времен, а где-то посередине, когда уже было реализовано много RPC. И вот кто-то попытался-таки навести порядок, начав уборку только в одном ящике стола в то время, как весь стол завален хламом. Так зачем же тогда ломать голову и себе и людям с прикручиванием DELETE и PUT? Особенно, когда нет полной структуры типа /ports/1/controllers/240/devices,
даже когда есть что-то подобное /devices/240 это все равно урезанная ресурсно-доменная версия, которая начинается где-то с середины.
Делайте в этом случае обычный RPC используя только POST и всё будет намного проще. 

А как же GET?!

С GET не все так однозначно. Запросить HTML страницу или сущность – нормально, но если запросить отчет на электронную почту или какую-то выборку с большим количеством фильтров, здесь возникают нюансы.

Во-первых, при большом количестве параметров трудно анализировать GET-запросы, выискивая глазками в URI нужное значение, особенно, когда там передается кириллица с процентным кодированием (encodeURI), ну вы поняли.

Во-вторых, ограничение длины. Не сталкивались? URI не бесконечный. 2047 символов для IE11 и Edge, тот же Apache по умолчанию может около 8к, а если у вас параметр на входе принимает несколько сотен значений, а если таких параметров несколько…

В-третьих, (это особый случай для API, но все же имеет место) чувствительная информация в URI GET-запросов может:

  • Сохраняться в логах веб-серверов и прокси

  • Передаваться через заголовки referrer между страницами

  • Отображаться в истории браузера

  • Попадать в аналитические системы

Хотя современные браузеры и поддерживают политики referrer, полагаться на них при проектировании API - рискованно.

И аргумент про удобство вызова из адресной строки браузера (я слышал его не раз) как-то не очень, если в методе есть заголовки.

В общем, резюмируя, могу сказать, что использовать GET целесообразно, когда:

  • число параметров не более 3-5;

  • значения параметров короткие – до 30 символов

  • не предполагается передача нескольких значений для одного параметра

  • не предполагается передача чувствительной информации

  • не используются специфические заголовки

  • нужно кэширование

Коды ошибок

Мнения расходятся, использовать их или нет. Например, JSON-RPC 2.0 вообще предлагает всегда отвечать 200 OK, это как бы намекает, что с транспортом ошибок не было. Но, на мой взгляд, это как-то очень скучно и сухо. А как же промежуточные системы, мониторинг и всякие шлюзы, которые, не умея анализировать сам запрос, могли бы принять какие-то решения на основе кода ошибки, уж это им под силу. А еще коды удобны, когда в случае ошибки парсинг ответа не предполагается: когда делается проверка типа if result != OK then return 0. Однако увлекаться кодами ошибок тоже не стоит. В свое время кодов придумали много, но, как показывает практика, в основном используется малая их часть. В разговорном языке люди тоже упрощают конструкции и это нормально – слово короче, а смысл тот же. Да, можно вообще одним известным словом изъясниться, склоняя его всячески, но это уже перебор ;)

Что в имени тебе моем?

Еще одна распространенная ситуация, которую хотелось бы подсветить. Когда делаете API, важно помнить один момент, которому, почему-то, не придают большого значения. Если вы создаете  интерфейс для внешнего потребителя (команды/системы), который не знаком с особенностями внутренней кухни, не используйте внутреннюю терминологию продукта/компании при именовании методов и/или параметров/атрибутов. Интерфейс должен быть понятен с первого взгляда на запрос и ответ. Когда вы смотрите на любое устройство впервые и видите на нем кнопку с изображением полукруга с вертикальной чертой внутри
( | ) и даже без надписи вкл/выкл, вы уже знаете, что она делает. С API должно быть точно так же. Простой пример – параметр называется MSISDN. Вы уже знаете, что в него передать? Наверняка знаете, если имели отношение к телекому, а если вы всю жизнь проработали в ритейле? Если делаете API для клиента с улицы, то он быстрее поймёт, что от него хотят, когда параметр будет называться «телефон» или «номер телефона» (PhoneNo). Или параметр Party. Думаете, речь про вечеринку? ))) Нет, речь про организацию/компанию. Уверен, многие скажут про корпоративную модель данных… КМД – это хорошо, но вы делаете интерфейс для клиента, а не для себя, поэтому забудьте про свою религию, наступите на горло предубеждениям, даже если они тысячу раз правильные.

Разумеется, если API предполагается использовать только внутри контура или даже внутри продукта, то КМД и общепринятые внутренние понятия наоборот облегчат понимание для разработчика. Но могут быть и такие глубинные вещи, которые знают только создатели конкретного продукта или процесса. Так бывает, что издревле что-то назвали неверно и, как говорят потом: "так исторически сложилось". И вот они (создатели) пытаются отразить глубинный смысл этого процесса в названии API-метода, при том, что сами создатели им (методом) пользоваться не планируют, они его делают для использования внутри контура, но другими командами. Здесь клиенты – это другие команды, они – потребители, для них делалось. Зачем вкладывать глубинный смысл в сложную аббревиатуру или набор слов? Упростите название так, как это выглядит со стороны для потребителей, а не для вас.

Заключение

Труъ RESTful – весьма специфичная штука, которая

  • Хорошо подходит для CRUD, но плохо для процессинга

  • GET приводит к трудностям при работе с крупными сущностями, когда у сущности сотни атрибутов, но они нужны не все и не всегда (про GraphQL отдельная история).

Я не предлагаю отказываться от REST, но его нужно использовать только там, где он уместен, где при завершении реализации у вас появится достаточное количество ресурсов с директориями, а не один отдельно стоящий метод (именно метод, а не endpoint) в пустыне. Там, где больше нужны процессинги, лучше использовать RPC (не тот, который JSON-RPC 2.0) по HTTP c системами путей и каталогов в конечных точках, без "Action":"Delete" в теле и без DELETE в HTTP-методе, но с глаголом в URL. То есть от REST нужно взять то лучшее, что в нем есть – система каталогизации, использование силы HTTP (коды ошибок, кэширование), а от RPC взять понятность, с точки зрения процедурного мышления. Двух методов GET и POST достаточно. DELETE, PUT, PATCH используйте только там, где к конечной точке применим весь набор этих методов вместе с GET и POST.

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

Ссылки по теме, если моих аргументов вам недостаточно:

REST vs JSON-RPC?

чтобы еще больше вас запутать: две антистатьи

Один

Два

Напоследок

Мартин Фаулер: "RESTful purity is not the goal - meeting business needs is."

Рой Филдинг: "REST is software design on the scale of decades: every detail is intended to promote software longevity and independent evolution."

Немного психологии или почему все так произошло с REST

Практика показывает, что людям понятнее/привычнее, когда есть глаголы, потому что большинство самых распространенных языков программирования это императив. Поэтому такая модель подсознательно транслируется и в методы (в их названия). Появление такого количества императивных языков и их популярность именно в том, что это совпадает с психологией человека. Человеку привычнее сказать "иди копать яму" (в бэкэнде сплошной императив), а не "яма должна быть выкопана" (декларатив). Это психология, ничего с этим не поделать. И во всей этой причинно-следственной связи вызывает диссонанс лишь то, что есть HTTP-методы (GET, POST, PUT и т.д.), которые сложно укладываются в голове в контексте команд. Приходится переключать мышление на ресурсный уровень. Нужно думать с позиции домена и как-то это уложить в логическую модель, а мозг говорит - глагол! Надо глагол! Хотя он и есть, этот глагол, но не в имени эндпоинта, а спрятан под капот HTTP. С одной стороны классический REST очень красивый, с другой стороны он слишком противоречивый. Почему все за него цепляются не понятно. Хотя нет - все понятно! Потому что не появился новый Филдинг, который бы презентовал новую хайповую модель. У всех людей всегда проблема с выбором. Людям нужен кто-то, кто скажет как правильно делать. И как бы не рассказывали про то, что люди сами хотят принимать решения - враньё! В глубине души большинство хотят, чтобы кто-то принял решение за них, какой-нибудь Филдинг.

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


  1. pilot114
    11.10.2025 12:36

    вы пару раз критически упоминули json-rpc 2.0, но не раскрыли в чём его минусы.

    Почему там всегда 200 понятно - изза батчинга. А что ещё?


    1. supercat1337
      11.10.2025 12:36

      Один из недостатков: json-rpc в спецификации не гарантирует порядок результатов при пакетных запросах. Но я бы не сказал, что это фатально. Если разработчику важен порядок в массиве результатов, то он просто учтет этот момент у себя в коде. То, что нет четко закреплённого списка ошибок - тоже спорный момент.

      Статус 200 - это просто успешный транспорт payload. И дело тут не в батчинге.


    1. rt001 Автор
      11.10.2025 12:36

      Я вовсе не критиковал json-rpc 2.0, а лишь приводил его, как противовес. Но не рассматривал его как альтернативу, потому что и в нем всё не так, как разработчикам хотелось бы интуитивно. Он не плохой, со своими задачами справляется, но, как сказал Билли, в исполнении Караченцова, в фильме Человек с бульвара капуцинов:

      Вот так мы отдыхаем, Джонни. А душе хочется чего-то другого: светлого, большого...

      Каждый раз в статье, упоминая про json-rpc 2.0, я лишь подчеркиваю, что большинство реализаций псевдо-REST это по факту какой-то JSON-RPC или HTTP-RPC или как-то иначе его назвать, но он ничего не имеет общего с json-rpc 2.0, вот какая мысль звучит в статье. Название json-rpc уже занято.


  1. Kerman
    11.10.2025 12:36

    Но, на мой взгляд, это как-то очень скучно и сухо.

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


    1. rt001 Автор
      11.10.2025 12:36

      Ваш личный опыт - самый правильный учитель :)

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