Привет всем, на связи снова Дарья Борисова, системный аналитик из ПСБ. Продолжаю развеивать мифы о REST API.  В первой статье цикла мы разобрали фундаментальные заблуждения о природе REST. Сегодня переходим к более прикладным, но не менее спорным вопросам — к мифам о реализации. Мы разберем тонкости работы с методами, поговорим о настоящем смысле «stateless» и выясним, правда ли, что новые технологии отправляют REST на покой. Погружаемся глубже.

Миф 1. PUT и PATCH — одно и то же, можно использовать любой метод

Миф

И PUT, и PATCH используются для обновления данных на сервере. Разница какая-то семантическая, но на практике можно отправлять одни и те же данные любым из этих методов. Главное — чтобы работало. Если один не сработал, пробуем другой.

Типичные проявления:

  • Разработчик отправляет полное представление ресурса через PATCH.

  • Разработчик отправляет только измененные поля через PUT.

  • Серверная логика обрабатывает оба метода идентично.

Реальность

PUT и PATCH — это принципиально разные операции с четко определенной в HTTP семантикой. Их нельзя использовать взаимозаменяемо, так как это нарушает контракт между клиентом и сервером, ведет к неожиданным ошибкам и усложняет понимание API.

Ключевое отличие: PUT — операция полной замены, а PATCH — операция частичного изменения.

Объяснение

1. Семантика

Метод PUT запрашивает замену текущего представления целевого ресурса данными из запроса. Клиент должен отправить полное и валидное представление ресурса. Если вы отправите только поле title, сервер интерпретирует это как: «Я хочу, чтобы у ресурса было ТОЛЬКО поле title. Все остальные поля (author, content и так далее) должны быть удалены».

Метод PATCH запрашивает применение набора изменений, описанных в теле запроса, к целевому ресурсу. Клиент отправляет только те данные, которые нужно изменить. Сервер применяет эти изменения к существующему ресурсу, оставляя нетронутыми все остальные поля.

2. Идемпотентность — ключевое свойство

PUT — идемпотентен. Это значит, что многократное выполнение одного и того же PUT-запроса должно приводить к одному и тому же результату на сервере. Если после первого запроса ресурс был заменен, то второй, третий и десятый идентичный запрос ничего не изменят. 

PATCH — НЕ идемпотентен по умолчанию. Многократная отправка одного и того же PATCH-запроса может каждый раз менять состояние ресурса. Например, запрос { "op": "increment", "path": "/views", "value": 1 } при каждом выполнении будет увеличивать счетчик. Чтобы PATCH был идемпотентным, операция должна быть объявлена таковой (например, { "op": "replace", "path": "/title", "value": "Новое название" } — идемпотентна).

Допустим, у нас есть ресурс Статья:

{
"id": 123,
"title": "Старая статья",
"content": "Старый текст",
"author": "Иван",
"views": 100
}

Задача: Изменить только title.

• Неправильно (но часто делают):

    – PUT /articles/123 с телом {"title": "Новая статья"}.

    – Результат (ожидаемый по спецификации): Ресурс будет полностью заменен. Сервер сохранит только title. Поля content, author, views будут удалены. Это почти наверняка ошибка.

• Правильно с использованием PATCH:

    – PATCH /articles/123 с телом {"title": "Новая статья"}.

    – Результат: Изменится только title. Все остальные поля (content, author, views) останутся нетронутыми.

• Правильно с использованием PUT (если очень хочется):

    – PUT /articles/123 с телом {"title": "Новая статья", "content": "Старый текст", "author": "Иван", "views": 100}.

    – Клиент должен сначала получить (GET) полное текущее состояние, изменить в нем нужное поле, а затем отправить полную версию обратно через PUT. Это неэффективно и подвержено состоянию гонки (данные могли измениться между GET и PUT).

Итог

Миф о взаимозаменяемости PUT и PATCH возникает из‑за поверхностного сходства («оба обновляют объект») и недостаточного внимания к семантике HTTP. Использование PUT для частичного обновления — это антипаттерн, который ломает идемпотентность и ведет к потере данных. Правильное разделение: PATCH — ваш основной инструмент для модификаций, PUT — узкоспециальный инструмент для полной замены, когда клиент знает абсолютно всё о ресурсе. Следование этому правилу делает API предсказуемым, надежным и соответствующим стандартам.

Миф 2. REST stateless (не имеет состояния), значит, нельзя использовать сессии

Миф

Использование принципа Stateless в REST означает, что сервер вообще не должен ничего помнить о клиенте между запросами. Любое сохранение состояния на сервере — например, сессия с session ID в cookies — это автоматически нарушение принципа stateless и, следовательно, уже не REST. Настоящее REST API должно каждый раз заново аутентифицировать пользователя, передавая логин и пароль в каждом запросе, и не может хранить его 'корзину покупок' или 'текущую страницу' на сервере.

Типичные проявления:

  • Разработчики сознательно избегают механизмов сессий, считая их«не‑RESTful».

  • API требует отправки полных учетных данных (логин/пароль, токен) в теле каждого запроса, а не в заголовке Authorization.

  • Критика любого серверного хранения данных о клиенте как «нарушающего REST».

  • Путаница между состоянием приложения (application state) и состоянием аутентификации/сессии (client session state).

Реальность

Принцип stateless в REST относится исключительно к состоянию приложения, а не к состоянию аутентификации или сессии. Сервер может и должен хранить данные пользователей, их права, токены доступа и сессионную информацию. Запрещено хранить на сервере контекст, необходимый для понимания запроса — этот контекст должен полностью содержаться в самом запросе.

Ключевое различие: Stateless означает, что один запрос содержит всю информацию, необходимую серверу для его обработки. Сервер не должен полагаться на контекст, сохраненный от предыдущих запросов этого же клиента.

Объяснение

1. Что такое состояние? 

В контексте REST и клиент-серверного взаимодействия есть три типа состояния:

  • Состояние ресурса. Это данные, хранящиеся в базе данных сервера (пользователи, заказы, статьи). Оно всегда есть на сервере. REST как раз и занимается управлением этим состоянием через запросы GET, POST, PUT, DELETE.

  • Состояние приложения на клиенте. Это то, на какой "странице" находится клиент, какие данные он уже получил, какую форму заполнил. В веб-приложениях это часто хранится в памяти браузера или в localStorage. Сервер об этом не знает и знать не должен.

  • Состояние сессии на сервере. Это информация, связывающая несколько запросов от одного клиента. Сюда относится аутентификация ("этот запрос от пользователя Ивана"), временные данные сессии (корзина покупок до оформления заказа), флаги.

Принцип stateless в REST говорит: Сервер не должен хранить Состояние приложения на клиенте между запросами.

2. Что нарушает stateless

Сервер НЕ ДОЛЖЕН помнить контекст предыдущего запроса, чтобы понять следующий.

Пример нарушения (классический):

1. Клиент: GET /search?page=1 (Сервер«запоминает», что клиент на странице 1).

2. Клиент: GET /search?page=next (Сервер, используя сохраненный контекст («он был на page=1»), интерпретирует это как запрос страницы 2).

Почему это плохо: Если балансировщик нагрузки направит второй запрос на другой сервер, тот не будет знать о page=1. Запрос page=next бессмысленен без контекста.

Исправление (stateless):

Клиент должен сам управлять своим состоянием приложения и передавать весь необходимый контекст:

1. Клиент: GET /search?page=1&limit=20

2. Сервер отвечает, включая в ответ ссылки (HATEOAS), например, "next": "/search?page=2&limit=20".

3. Клиент: GET /search?page=2&limit=20 (явный, самодостаточный запрос).

3. Что разрешено и даже необходимо (НЕ нарушает stateless)

Сервер МОЖЕТ и ДОЛЖЕН хранить персистентные данные, связанные с клиентом, для обеспечения безопасности и функциональности.

Примеры, которые НЕ являются нарушением:

  • Аутентификация через токен: Клиент отправляет Authorization: Bearer <JWT_Token> в заголовке каждого запроса. Сервер проверяет токен (может для этого обращаться в БД или кеш). Токен — это и есть вся необходимая информация для аутентификации, содержащаяся в запросе. Хранение mapping токен -> пользователь на сервере — это нормально.

  • Сессия в классическом виде (Session ID в cookie): Клиент отправляет cookie с sessionid=abc123 в каждом запросе. Сервер по этому ID находит в БД или кеше данные сессии (user_id, права). Cookie — это часть запроса, делающая его самодостаточным. Это технически соответствует stateless! Проблема сессий не в stateless, а в масштабируемости (привязка к серверу) и в том, что они часто используются для хранения состояния приложения, что уже является нарушением.

  • Хранение корзины покупок на сервере: До момента оформления заказа корзина — это ресурс. Клиент взаимодействует с ней через RESTful-интерфейс:

    •   POST /carts – создать корзину, получить ее ID.

    •   PUT /carts/{cart_id}/items – добавить товар.

    •   GET /carts/{cart_id} – просмотреть корзину.

ID корзины (cart_id) клиент передает в каждом запросе. Сервер хранит корзину в БД. Это состояние ресурса, а не контекст сессии приложения. Полностью stateless.

Почему этот миф так живуч?

  1. Путаница с масштабируемостью: Stateless-архитектура облегчает горизонтальное масштабирование, так как любой сервер может обработать любой запрос. Сессии с привязкой к памяти конкретного сервера усложняют это. Миф подменяет понятие: «сессии усложняют масштабирование» на «сессии нарушают REST».

  2. Исторический контекст: Рой Филдинг в своей диссертации критиковал использование серверных сессий для хранения состояния приложения (как в примере с page=next). Сообщество упростило это до "сессии — зло".

  3. Популярность токенов JWT: JWT, который часто передается в заголовке, воспринимается как «более RESTful», чем cookie с session ID, хотя с точки зрения принципа stateless они эквивалентны — оба являются самодостаточными учетными данными в запросе.

Итог

Ключ к соблюдению stateless — в том, чтобы каждый HTTP-запрос был самодостаточной единицей, несущей в себе все необходимое для своей обработки (учетные данные, ID ресурсов, параметры). А то, как сервер валидирует эти учетные данные (по токену в БД или JWT) или хранит связанные с пользователем ресурсы (корзины, профили) — это внутренняя реализация, не нарушающая архитектурных ограничений REST.

Если хотите проверить себя, задайте вопрос:«Если бы клиент отправил этот же запрос через минуту, или отправил его на другой сервер кластера, был бы результат для этого конкретного запроса таким же?»

  • Если ДА (например, запрос GET /users/me с валидным токеном всегда вернет данные владельца токена) — вы соблюдаете stateless. Сервер может использовать свою БД для поиска данных пользователя по токену.

  • Если НЕТ (например, запрос POST /checkout/next-step без дополнительных данных зависит от того, на каком шаге оформления заказа вас «запомнил» сервер) — вы нарушаете stateless. Вместо этого нужно POST /checkout/steps с явным указанием всех данных текущего шага в теле запроса.

Миф 3. «GraphQL/gRPC — это 'новый REST' или его прямая замена»

Миф

REST устарел. GraphQL и gRPC — это его эволюция и просто более современный и эффективный способ делать то же самое — связывать клиент и сервер. GraphQL и gRPC решают все проблемы REST’а (over-fetching, under-fetching, много эндпоинтов). Нужно просто взять новую технологию и переписать на ней старый REST API.

Реальность

GraphQL, gRPC и REST — это решения для разных архитектурных задач, основанные на разных парадигмах. Они не являются взаимозаменяемыми. Это не эволюция «от плохого к хорошему», а выбор между разными компромиссами в спектре «свобода клиента vs контроль сервера», «гибкость данных vs эффективность сети», «гипермедиа vs строгий контракт».

Ключевая аналогия: Спросить«Что лучше заменит REST: GraphQL или gRPC?» — всё равно что спросить «Что лучше заменит дорогу: рельсы или воздушный коридор?». И то, и другое — транспорт, но с фундаментально разными свойствами, ограничениями и областями применения.

Объяснение

REST, GraphQL и gRPC – это 3 разных философии. Подробно обсудим каждую.

1. REST — это архитектурный стиль, а не протокол или технология.

  • Суть: клиент-сервер, stateless, кеширование, единообразие интерфейса, слоистая система.

  • Что это даёт? Систему, где сервер динамически управляет клиентом через гипермедиа-ссылки. Клиент знает только точку входа и семантику медиа-типов. Переходы могут (но этим мало кто пользуется) определяться ссылками в ответах сервера. Это дает невероятную гибкость серверу для эволюции API без breaking changes для клиентов.

Ключевой компромисс: REST — это про долгосрочную эволюцию системы, слабую связность и независимое развертывание клиентов и серверов. Идеально для долгоживущих публичных API или сложных enterprise-экосистем, где вы не контролируете всех клиентов.

2. GraphQL — это язык запросов и средство выполнения для API.

  • Суть: Декларативный язык, позволяющий клиенту точно запросить нужные ему данные в одной операции. Единая endpoint (обычно /graphql), куда клиент отправляет документ-запрос, описывающий структуру требуемых данных. Сервер исполняет этот запрос, собирая данные из различных источников.

  • Что это даёт? Решение проблем over-fetching (избыточной выборки) и under-fetching (недостаточной выборки). Мощные инструменты для клиентов (особенно мобильных), которые хотят контролировать форму ответа. 

Ключевой компромисс: Передача контроля от сервера к клиенту. Клиент становится сильно связанным со схемой данных. Это усложняет кеширование на уровне сети (HTTP-кеширование ломается), может приводить к проблемам N+1 и требует сложной реализации на стороне сервера для контроля прав доступа и сложности запросов.

GraphQL — это про эффективность разработки клиентов и гибкость в получении данных. Идеально для ситуаций, где у вас есть один мощный сервер (или gateway), множество клиентов (особенно мобильных) с разными требованиями к данным, и вы готовы инвестировать в сложность серверной реализации. Это не замена REST, а альтернативный способ организации слоя данных, часто используемый внутри архитектурного стиля (например, за GraphQL Gateway может стоять множество REST-сервисов). Важно помнить, что для неконтролируемых клиентов такой подход может не сработать.

3. gRPC — ��то высокопроизводительный фреймворк для RPC 

  • Суть: Позволяет клиенту вызывать методы на удалённом сервере так, как если бы это были локальные методы. Основан на HTTP/2, бинарном формате protobuf и строгом компилируемом контракте (.proto-файл).

  • Что это даёт? Максимальную производительность (низкая латентность, бинарный протокол, мультиплексирование потоков через HTTP/2), строгую типизацию, автоматическую генерацию клиентского и серверного кода. Идеален для синхронной связи между сервисами внутри одного домена, т.е микросервисы в backend.

Ключевой компромисс: Жёсткая, компилируемая связность. Изменение контракта требует перегенерации и перекомпиляции кода у всех участников. Практически нулевая читаемость для человека. Не предназначен для использования напрямую из браузера и для публичных API для неподконтрольных клиентов.

gRPC — это про скорость и эффективность внутренней коммуникации. Это не замена REST, а замена более старым RPC-механизмам, XML-RPC, SOAP или даже «тяжёлым» REST-вызовам внутри backend. Это инструмент для внутреннего обмена, а не для публичного фасада.

Сводная таблица, дающая ответ на вопрос: "Когда что выбирать?"

Критерий

REST

GraphQL

gRPC

Основная абстракция

Ресурс (существительное)

Граф и запрос к нему

Сервис и метод (глагол)

Протокол / Формат

HTTP/1.1-2, JSON/XML

HTTP (чаще POST), свой язык запросов

HTTP/2, бинарные Protocol Buffers

Контракт

Неявный (OpenAPI/Swagger — документация)

Явная схема (типы, связи)

Явный, строгий, компилируемый (.proto)

Кто управляет запросом?

Сервер (фиксированные эндпоинты, HATEOAS)

Клиент (формирует произвольный запрос)

Сервер (фиксированные методы)

Кеширование

Отличное (на уровне HTTP)

Сложное (нужен клиентский кеш, например, Apollo)

Сложное (реализуется вручную)

Эволюция API

Гибкая (новые поля, HATEOAS)

Гибкая (добавление полей), но ломающие изменения в схеме сложны

Сложная (требует версионирования контракта и синхронного обновления) 

Идеальный use-case

Публичные API, долгоживущие системы, веб-приложения, где важна discoverability и эволюция

Сложные клиенты (мобильные) с разными view, агрегация данных из многих источников (BFF).

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

Связность

Низкая (клиент знает только медиа-типы и ссылки) 

Высокая (клиент знает схему данных) 

Очень высокая (клиент и сервер разделяют скомпилированный контракт)

Итог

GraphQL и gRPC не решают проблемы плохого дизайна API. Если у вас хаос в доменной модели, отсутствие чётких границ контекстов, переход на новую технологию только усугубит проблему, обернув её в новый синтаксис.

«REST устарел» — это часто признак непонимания REST. Многие говорят «REST», имея в виду «HTTP API с JSON, спроектированный без понимания гипермедиа». Да, такие API часто негибки. Но это проблема их реализации, а не архитектурного стиля. Настоящий REST с HATEOAS — это мощный инструмент для определённого класса проблем, и он не устарел.

Заключение

Задача аналитика и архитектора — не гнаться за модным термином, а понять глубинные компромиссы каждой технологии и предложить архитектуру, где каждый инструмент используется в своей сильной зоне. Правильный вопрос не "На чём делать API?" или “Какой тип метода использовать?”, а "Какую проблему бизнеса или разработки мы решаем и какой набор технологий лучше всего для этого подходит?".

Я не разобрала ещё несколько распространённых мифов. Каких? Узнаете в следующей части! (Или можете предположить в комментариях ;))

В своем тг-канале Мастерская IT-решений рассуждаю о System Design. Присоединяйтесь!