Несколько лет назад я разработал новую информационную систему в компании, которая занималась телекоммуникациями. Нам приходилось взаимодействовать с большим количеством веб-сервисов, сталкиваясь с устаревшими системами и бизнес-партнерами.
Нечего и говорить, что мы достаточно настрадались с SOAP. Непонятные WSDL, несовместимые библиотеки, странные баги… Когда мы могли, мы использовали протоколы вызова удаленной процедуры: XMLRPC, JSONRPC.
Наши первые серверы и клиенты для этих протоколов были очень простыми, ограниченными, хрупкими. Со временем мы улучшали их. Спустя пару сотен строчек дополнительного кода мы исполнили нашу мечту. Мы смогли поддерживать различные диалекты (например, специфичные для Apache расширения XMLRPC). Были добавлены встроенные преобразования для исключений Python и иерархических кодов ошибок. Раздельно обрабатывались функциональные и технические ошибки, причем в последнем случае автоматически проводился перезапуск. Мы добавили уместную регистрацию и выдавали статистику до и после запросов, тщательно проверяли вводные данные…
Теперь мы могли прочно подключиться к любому API. Для этого требовалась всего пара строчек кода.
Теперь мы могли представить любой набор функций широкой публике, серверам и веб-браузерам. Для этого требовалось несколько декораторов и обновлений.
Что касается связи между разными приложениями (в виде микросервисов), этим занимался наш системный администратор. Со стороны ПО этот процесс был практически прозрачным.

Разработчик отдыхает после трудной получасовой интеграции RPC API
Тогда появился REST.
Representional State Transfer – передача состояния представления.
Волна обновления захлестнула основы межсервисной связи.
RPC погиб, а будущее было за RESTful: ресурсы, живущие на отдельных URL и управляющиеся через протокол HTTP.
С тех пор любой API, который нам нужно было передать или принять, становился новой трудностью, приближал к безумию.
Чем плох REST?
Вместо долгих рассуждений просто приведем небольшой пример. Ниже представлен API. Типы данных убраны для того, чтобы было легче читать:
createAccount(username, contact_email, password) -> account_id
addSubscription(account_id, subscription_type) -> subscription_id
sendActivationReminderEmail(account_id) -> null
cancelSubscription(subscription_id, reason, immediate=True) -> null
getAccountDetails(account_id) -> {full data tree}
Достаточно добавить хорошо задокументированную иерархию исключений (InvalidParameterError, MissingParameterError, WorkflowError...), подклассы, нужные для того, чтобы идентифицировать важные случаи (например, AlreadyExistingUsernameError). Готово!
Этот API достаточно просто понять, им легко пользоваться. Он поддерживается точной машиной состояний. Из-за ограниченного набора доступных операций пользователи не могут делать что-либо бессмысленное (например, изменять дату создания аккаунта).
Приблизительное время, необходимое для раскрытия этого API как простого сервера RPC: несколько часов.
Теперь пора пойти по пути RESTful.
Больше нет стандартов и точных спецификаций. Только расплывчатая «философия RESTful», подверженная бесконечным метафизическим дебатам. Применяется множество отвратительных обходных путей.
Как точно перевести представленные выше функции в операции CRUD? Отправление сообщения об активации – это обновление атрибута «must_send_activation_reminder_email»? Или же это создание ресурса «activation_reminder_email»? Разумно ли использовать DELETE для cancelSubscription(), если подписка остается активной на время отсрочки и может быть восстановлена в этот период? Как разбить дерево данных getAccountDetails() между конечными точками, сохраняя модель данных REST?
Какую URL присваивать каждому из «ресурсов»? Да, это просто, но ведь все равно приходится делать.
Как выражать разнообразие состояний об ошибках, используя ограниченное количество кодов HTTP?
Какие форматы сериализации, какие конкретно диалекты применять для ввода и вывода данных?
Как именно распределить сигнатуры между методом HTTP, URL, строками запроса, полезной нагрузкой, хэдерами, кодом статуса?
Теперь вам придется часами заново изобретать колесо. Причем это колесо не будет хорошо сделанным, продуманным. Это скорее сломанное, хрупкое колесо, для которого требуются тонны документации, чтобы его работу вообще можно было понять. Здесь вы нарушаете спецификации, даже не подозревая об этом.

Стоило назвать его не REST, а WORK.
Погрузимся глубже в искусственные проблемы, созданные из-за философии этой разработки.
Веселые глаголы REST
REST – это не CRUD; его последователи позаботятся о том, чтобы вы их не путали. Пару минут позже они будут радоваться тому, что методы HTTP имеют хорошо определенную семантику для создания (POST), получения (GET), обновления (PUT/PATCH) и удаления (DELETE) ресурсов.
Они будут рады заявить, что этих нескольких «глаголов» достаточно, чтобы выразить любую операцию. Ну, конечно. А горстки глаголов было бы достаточно, чтобы выразить любое понятие на русском языке: «Сегодня я обновил свое ВодительскоеСиденье своим телом и создал ЗапускДвигателя, но БакДляТоплива удалил сам себя». То, что это возможно, не значит, что от этого язык не становится менее неловким. Может, таким людям просто нравится токипона?
Если все дело в минимализме, то стоит все хотя бы сделать правильно. Знаете ли вы, почему PUT, PATCH и DELETE никогда не были реализованы в формах веб-браузеров? Потому что они бесполезны и вредны. Можно просто использовать GET для чтения и POST для записей. Можно применять только POST, когда кэширование на уровне HTTP нежелательно. Другие методы в лучшем случае будут мешать вам, а в худшем – испортят ваш день.
Хотите использовать PUT для обновления ресурса? Хорошо, но некоторые Священные Спецификации утверждают, что ввод данных должен быть эквивалентен представлению, полученному через GET. Итак, что вы делаете с многочисленными параметрами, возвращаемыми GET (время создания, время последнего обновления, сгенерированный сервером токен…)? Вы их опускаете и нарушаете принципы PUT? Вы все равно включаете их и ожидаете «HTTP 409 Conflict», если они не совпадают со значениями на стороне сервера (из-за чего приходится воспользоваться GET)? Вы придаете им случайные значения и ожидаете, что серверы их проигнорируют (радость тихих ошибок)? Выбирайте свой яд. REST явно не имеет ни малейшего понятия, что такое атрибут, доступный только для чтения. Это не будет исправлено в ближайшее время. Между тем, GET должен возвращать пароль (или номер кредитной карты), который был отправлен в предыдущем POST/PUT. Удачи в работе с параметрами, доступными только для записи.
Я забыл упомянуть, что PUT также добавляет опасные состояния гонки, когда несколько клиентов отменяют изменения друг друга, пытаясь обновить разные поля.
Хотите использовать PATCH для обновления ресурса? Хорошо, но, как и 99% людей, использующих этот глагол, вы просто отправите подмножество полей ресурсов в запросе, надеясь, что сервер правильно поймет операцию (и все ее возможные побочные эффекты).Многие параметры ресурсов тесно связаны между собой или взаимоисключают друг друга (например, это либо кредитная карта, либо токен PayPal в платежной информации пользователя). Дизайн RESTful также скрывает эту важную информацию. В любом случае, вы нарушите спецификации еще раз: PATCH не должен просто отправлять множество полей для переопределения. Вместо этого вы должны предоставить «набор инструкций», которые будут применяться к ресурсам. Итак, вам опять придется решать, как выразить эти инструкции. Часто придется пользоваться вручную сделанными спецификациями из-за синдрома «это не изобретено здесь», который де-факто является стандартом в мире REST.
Хотите удалить ресурсы? Хорошо, но я надеюсь, что вам не нужно предоставлять существенные контекстные данные, такие как PDF-скан запроса на удаление от пользователя. DELETE запрещает иметь полезную нагрузку. Архитекторы REST часто игнорируют это ограничение, поскольку большинство веб-серверов не применяют это правило к полученным запросам. Насколько совместим запрос DELETE с присоединенной строкой запроса base64 в 2 МБ?

Поклонники REST API легко заявляют, что «люди делают все неправильно», а их API «на самом деле не RESTful». Например, многие разработчики используют PUT для создания ресурса непосредственно по его окончательному URL (/myresourcebase/myresourceid
). «Правильный способ» сделать это - POST на родительском URL (/myresourcebase
), а сервер пусть с помощью заголовка HTTP «Location» указывает URL-адрес нового ресурса. Хорошая новость: это не имеет значения. Эти строгие принципы занимают философов часами, но очень мало влияют на реальные жизненные проблемы.
Кстати... Ручная работа с URL всегда доставляет удовольствие. Знаете ли вы, сколько существует реализаций правильных идентификаторов для urlencode() при создании REST URL? Не так много. Будьте готовы к неприятным багам и атакам SSRF/CSRF.

Когда вы забыли применить urlencode к именам пользователей в одном из 30 своих составленных вручную URL-ов
Обработка ошибок в REST
Каждый программист может сделать «номинальный случай». Обработка ошибок - один из параметров, который будет определять, является ли ваш код надежным или нет.
HTTP предоставляет встроенный список кодов ошибок. Отлично, давайте посмотрим на них.
Использование «HTTP 404 Not Found» для уведомления о несуществующем ресурсе звучит идеально для RESTful, не так ли? Жаль, что nginx был неправильно настроен на 1 час, поэтому пользователи API получили всего 404 ошибки и забросили сотни учетных записей, думая, что они были удалены…

Наши пользователи после того, как мы по ошибке удалили гигабайты их картинок с котиками
Использование «HTTP 401 Unauthorized», когда пользователь не имеет учетных данных для доступа к сторонней службе, звучит приемлемо, не так ли? Однако, если вызов ajax в браузере Safari получает этот код ошибки, он может испугать вашего конечного пользователя очень неожиданным запросом пароля.
HTTP существовал задолго до «RESTful веб-сервисов», и веб-экосистема полна предположений о значении его кодов ошибок. Использование их для транспортировки ошибок приложений неизбежно приведет к катастрофе.
Некоторые стандартные коды ошибок HTTP являются специфическими для Webdav, другие - для Microsof. Некоторые из оставшихся кодов имеют настолько нечеткие определения, что их невозможно понять. В конце концов, как и большинство пользователей REST, вы, вероятно, будете использовать случайные HTTP-коды, такие как «HTTP 418 Я чайник» или неназначенные числа, для выражения исключений для вашего приложения. Можно бесстыдно возвращать «HTTP 400 Bad Request» для всех функциональных ошибок, а затем придумывать свой собственный неуклюжий формат ошибок с булевыми, целочисленными кодами, слагами и переведенными сообщениями, вставленными в произвольный запрос. Можно вообще отказаться от правильной обработки ошибок. Вы просто вернете простое сообщение на естественном языке и понадеетесь, что пользователь сможет проанализировать проблему и принять меры. Удачи во взаимодействии с такими API из автономной программы.
Концепции REST
Репутация REST построена на концептах, которые любой сервисный архитектор должен уважать, и принципах, которым не следует даже сама система. Ниже представлены отрывки из наиболее популярных веб-страниц.
REST – клиент-серверная архитектура. Клиент и сервер имеют различные особенности.
Какая же свежая концепция в мире ПО.
REST предоставляет единообразный интерфейс для разных компонентов.
Ну, то же самое делает любой другой протокол, если это лингва франка всей экосистемы сервисов.
REST – многослойная система. Отдельные компоненты не могут «смотреть» за пределы слоя, с которым они взаимодействуют.
Похоже, это естественное последствие работы любой хорошо проработанной, слабо связанной архитектуры. Восхитительно.
REST прекрасен, ведь он не имеет состояний. Но за веб-сервисом стоит огромная БД, которая не помнит состояния клиента. Может, она помнит сеанс аутентификации, права доступа… Но состояний здесь нет. Если точнее, REST API не имеет состояний, как и любой протокол на основе HTTP, как простой RPC, упоминавшийся ранее.

С REST можно использовать мощь HTTP-кэширования! Ну и напоследок: запрос GET и его заголовки для управления кэшем совместимы с веб-кэшем. При этом не достаточно ли локальных кэшей (Memcached и т. д.) для 99% веб-сервисов? Неуправляемые кэши - опасные звери. Сколько людей захочет представить свои API в виде открытого текста, чтобы Varnish или Proxy предоставляли устаревший контент даже после того, как ресурс будет обновлен или удален? Может быть, они будут отправлять его «вечно», если однажды произошла ошибка конфигурации? Система должна быть безопасной по умолчанию. Я прекрасно признаю, что некоторые сильно загруженные системы хотят получить выгоду от кэширования HTTP. Гораздо дешевле выставить несколько конечных точек GET для тяжелых взаимодействий, чем переключать все операции на REST и обработку сомнительных ошибок.
Благодаря всему этому REST имеет высокую производительность!
Вы уверены? Любой разработчик API знает: локально нужны очень точные API, способные делать что угодно; удаленно нужны общие API, чтобы ограничить влияние сетевых циклов. В этом аспекте REST тоже проваливается. Разрыв данных между «ресурсами», нахождение каждой версии на отдельной конечной точке приводит к проблеме запроса N+1. Чтобы получить полный набор данных пользователя (аккаунт, подписки, платежные реквизиты…), нужно отправить большое количество HTTP-запросов. Их нельзя проводить параллельно, ведь вы не можете знать заранее уникальные ID соответственных ресурсов. Из-за этого, а также из-за неспособности получать только часть ресурсов и получаются «бутылочные горлышки».
REST предоставляет лучшую совместимость.
В смысле? Почему у такого большого количества веб-серверов на REST есть «/v2/» или «/v3/» в базовых URL? Получить API с восходящей совместимостью и наоборот не так уж и трудно, если пользоваться языками высокого уровня. Главное – следовать простым правилам при добавлении или удалении параметров. Насколько я знаю, в этом плане REST не добавляет ничего нового.
REST – это просто. Все знают HTTP!
Ну, всем также знакома галька, но для постройки дома почему-то ищут более удобные блоки. Именно поэтому XML – это метаязык, а HTTP – метапротокол. Чтобы создать настоящий протокол приложения (как «диалекты» для XML), нужно уточнить множество вещей. В итоге вы получите очередной RPC-протокол, как будто их еще слишком мало.
REST очень прост, запросы можно отправлять с любой оболочки, с CURL!
Вообще-то на любой протокол, основанный на HTTP, можно отправлять с помощью CURL. Это касается даже SOAP. Использование GET – это особенно просто. Удачи с прописыванием json или xml POST вручную. Обычно люди используют файлы общего класса или, что гораздо более удобно, полноценные клиенты API, которые характеризуются прямо в командной строке на любимом языке.
Клиенту не нужно ничего знать о сервере, чтобы пользоваться им.
Пока что это моя любимая цитата. Я видел ее много раз, в разных формах, особенно с использованием модного слова HATEOAS. Иногда следовали осторожные фразы об исключениях. Тем не менее, я не знаю, в каком фэнтезийном мире живут эти люди, но в этом клиентская программа — это не колония муравьев. Она не просматривает удаленные API случайным образом, а затем решает, как лучше всего их обрабатывать, основываясь на распознавании образов или черной магии. Все наоборот. Клиент ПОМЕСТИТЬ (PUT) одно поле к URL с конкретным значением. Серверу лучше уважать семантику, которая была согласована во время интеграции, иначе наступит сущий ад.

Когда спрашиваешь, как же работает HATEOAS
Как правильно и быстро пользоваться REST?
Забудьте о правильности. REST – это как религия. Ни один смертный никогда не сможет осознать ее гения или сделать что-то правильно.
Возникает вопрос. Если вам приходится передавать или получать сервисы в стиле RESTful, как закончить эту задачу побыстрее и перейти к более конструктивным заданиям?
Как индустриализировать экспозицию на стороне сервера?
Каждый веб-фреймворк имеет собственный способ определения конечной точки URL. Ожидайте, что некоторые крупные зависимости или целый слой рукописного шаблона помогут подключить существующий API к вашему любимому серверу в качестве конечной точки REST.
Такие библиотеки, как Django-Rest-Framework, автоматизируют создание API-интерфейсов REST, выступая в качестве ориентированных на данные оболочек над схемами SQL/noSQL. Если вы просто хотите пользоваться методом «CRUD over HTTP», вам будет хорошо с ними. Если вы хотите использовать общие API-интерфейсы в стиле «сделай за меня» с рабочими процессами, ограничениями, сложным воздействием на данные и т. д., вам будет нелегко «прогнуть» любую инфраструктуру REST под свои потребности.
Будьте готовы соединять каждый HTTP-метод каждой конечной точки с соответствующим вызовом метода. При этом велика доля ручной обработки исключений. Это нужно для того, чтобы преобразовать сквозные исключения в соответствующие коды ошибок и полезные нагрузки.
Как индустриализовать интеграцию на стороне клиента?
Из моего опыта: никак.
Для каждой интеграции API вам придется просматривать длинные документы и подробные рецепты о том, как должна выполняться каждая из N возможных операций.
Вам придется создавать URL-адреса вручную, писать сериализаторы и десериализаторы и учиться обходить неоднозначности API. Придется долго пользоваться методом проб и ошибок, прежде чем вы разберетесь с этим стилем.
Знаете ли вы, как поставщики веб-услуг компенсируют это и облегчают принятие?
Просто, они пишут собственные официальные клиентские реализации.
Для каждого крупного языка и платформы.
Недавно я имел дело с системой управления подписками. Они предоставляют клиенты для PHP, Ruby, Python, .NET, iOS, Android, Java... плюс некоторые внешние вклады для Go и NodeJS.
Каждый клиент живет в своем собственном хранилище GitHub. У каждого есть большой список версий, заявок на отслеживание ошибок и запросов на получение. У каждого свои примеры использования. У каждого своя неловкая архитектура, на уровне между ActiveRecord и RPC-прокси.
Это поразительно. Сколько времени уходит на разработку таких странных оболочек, тогда как нужно улучшать реальный, ценный, готовый к работе веб-сервис?

Сизиф разрабатывает Ещё Один Клиент для своего API
Заключение
Десятилетиями почти все языки программирования сталкивались с одним и тем же рабочим процессом: отправка входных данных для вызываемого объекта и получение результатов или ошибок в качестве выходных данных. Все было хорошо. Неплохо.
С Rest API это превратилось в безумную работу по превращению яблок в апельсины и восхвалению HTTP-спецификаций, которые тут же нарушаются.
Сейчас все более распространенными становятся микросервисы. Почему такая простая задача - связать библиотеки по сетям - остается настолько трудоемкой и обременительной?
Я не сомневаюсь, что некоторые умные люди предоставят случаи, когда REST проявляет положительные качества. Они продемонстрируют самодельный протокол на основе REST, позволяющий обнаруживать и выполнять операции CRUD на произвольных деревьях объектов благодаря гиперссылкам; они объяснят, что дизайн REST блестящий, а я просто не прочитал достаточно статей и диссертаций о его концепциях.
Мне все равно. Деревья узнаются по их плодам. То, что заняло у меня несколько часов программирования и работало очень надежно с простым RPC, теперь занимает недели и постоянно ломает ожидания. Вместо развития мы стали ковыряться в деталях.
Почти прозрачный удаленный вызов процедур был тем, что действительно требовалось 99% людей. Существующие протоколы, какими бы несовершенными они ни были, отлично справлялись со своей работой. Эта массовая мономания для наименьшего общего знаменателя сети, HTTP, в основном привела к огромной трате времени и серого вещества.
REST обещал простоту и добавил сложностей.
REST обещал надежность, а дал хрупкость.
REST обещал совместимость и обеспечил неоднородность.
REST — это новый SOAP.
Эпилог
Будущее могло бы быть ярким. Доступно множество гениальных протоколов в двоичном или текстовом форматах, со схемами и без них, с эффективным использованием новых способностей HTTP2… Давайте двигаться дальше. Нельзя вечно находиться в каменном веке веб-сервисов.