REST: ресурсы, представления, stateless

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

REST (Representational State Transfer) — это архитектурный стиль для веб-API, в котором всё описывается как ресурсы, ими управляют единообразно через HTTP, а каждый запрос самодостаточен.

REST — не протокол и не библиотека, а набор архитектурных ограничений, сформулированных Роем Филдингом в 2000 году. Если API им следует, его называют RESTful. Эти ограничения не прихоть: каждое из них даёт системе важное свойство — масштабируемость, простоту, надёжность. Разберём идею целиком, а затем самое практичное ограничение — stateless.

Ресурс против представления

Центральное понятие REST — ресурс. Ресурс — это любая значимая «вещь», о которой можно говорить: пользователь, заказ, статья, список комментариев. У каждого ресурса есть адрес — URL, который его идентифицирует:

/users/42          — конкретный пользователь
/users             — коллекция пользователей
/users/42/orders   — заказы пользователя 42

Здесь важна тонкость, которую часто упускают. Ресурс — это абстрактная сущность, а не конкретные байты. То, что сервер реально присылает клиенту, называется представлением (representation) ресурса. Один ресурс может иметь несколько представлений.

Пользователь 42 как ресурс — это «человек с такими-то свойствами». Его можно представить в JSON:

{"id": 42, "name": "Анна", "email": "[email protected]"}

А можно — в XML, в HTML-странице или даже в CSV. Это разные представления одного ресурса. Клиент заголовком Accept просит нужный формат, сервер отдаёт подходящее представление. Сам ресурс при этом не меняется — меняется только его «снимок».

Расшифруем теперь название: Representational State Transfer — «передача состояния через представления». Клиент и сервер обмениваются не объектами в памяти, а представлениями состояния ресурсов.

Шесть ограничений REST кратко

Филдинг описал шесть ограничений. Пять обязательных и одно опциональное:

ОграничениеСуть
Client-Serverразделение клиента (UI) и сервера (данные); развиваются независимо
Statelessсервер не хранит состояние клиента между запросами; каждый запрос самодостаточен
Cacheableответы помечаются как кэшируемые или нет; кэш ускоряет систему
Uniform Interfaceединый способ работы со всеми ресурсами (URL + HTTP-методы)
Layered Systemмежду клиентом и сервером могут стоять прокси и балансировщики незаметно
Code on Demandопционально: сервер может прислать исполняемый код (например JS)

Эти шесть пунктов — суть REST. Ниже разберём два самых важных для дизайна: uniform interface и (особенно подробно) stateless.

Uniform Interface — единый интерфейс

Это ограничение делает REST узнаваемым. Со всеми ресурсами работают одинаково: ресурс адресуется URL, а действие над ним задаётся стандартным HTTP-методом.

МетодСмыслПример
GETполучитьGET /users/42
POSTсоздатьPOST /users
PUTзаменить целикомPUT /users/42
DELETEудалитьDELETE /users/42

Прелесть в предсказуемости: узнав, что есть ресурс /orders, вы сразу догадываетесь, что GET /orders/7 вернёт заказ, а DELETE /orders/7 удалит его — не читая документацию. Один набор «глаголов» на всю систему вместо тысяч уникальных функций.

Client-Server — разделение ответственности

Клиент отвечает за интерфейс и взаимодействие с пользователем; сервер — за хранение и логику. Они связаны только контрактом. Это позволяет, например, переписать мобильное приложение с нуля, не трогая сервер, и наоборот — масштабировать сервер, не выпуская новую версию приложения. Разделение — фундамент, на котором стоят остальные ограничения.

Stateless — почему сервер не хранит сессию

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

Сравним два подхода к авторизации.

Stateful (с сохранением состояния, НЕ по REST): при логине сервер создаёт сессию в своей памяти и шлёт клиенту короткий идентификатор. Дальше клиент шлёт только этот ID, а сервер по нему ищет в памяти, кто это.

1. POST /login           --> сервер запоминает: сессия abc = пользователь 42
2. GET /profile  (abc)   --> сервер ищет abc в памяти, узнаёт юзера 42

Stateless (по REST): сервер не хранит ничего. Клиент при каждом запросе предъявляет самодостаточный токен (например JWT), внутри которого зашита вся нужная информация о пользователе, подписанная сервером.

GET /profile HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Host: api.example.com

Сервер проверяет подпись токена, извлекает «пользователь 42» прямо из него — и не лезет ни в какую память о сессиях. Запрос самодостаточен.

Зачем это нужно — выгоды stateless

  • Масштабируемость. Если сервер не помнит клиентов, любой из десяти серверов за балансировщиком может обработать любой запрос. Не нужно «приклеивать» клиента к конкретному серверу.
  • Надёжность. Упал один сервер — клиент просто повторяет запрос на другой. Память с сессиями не теряется, потому что её нет.
  • Простота. Сервер не управляет временем жизни сессий, не чистит «протухшие» — меньше состояния, меньше багов.

Плата за это — каждый запрос немного «тяжелее»: токен и контекст приходится передавать каждый раз. Обычно это приемлемая цена за горизонтальное масштабирование.

Как работает под капотом

Покажем масштабируемость наглядно. Десять одинаковых серверов прячутся за балансировщиком. Клиент шлёт три запроса подряд — и каждый может попасть на разный сервер:

  клиент --> [ балансировщик ] --> сервер #3   (запрос 1, токен внутри)
  клиент --> [ балансировщик ] --> сервер #7   (запрос 2, токен внутри)
  клиент --> [ балансировщик ] --> сервер #1   (запрос 3, токен внутри)

Серверу #7 неважно, что предыдущий запрос обработал #3 — вся информация снова пришла в запросе. Если бы сервер #3 хранил сессию у себя в памяти, запрос 2 на сервере #7 сломался бы: «не знаю такого клиента». Именно поэтому stateless и масштабируемость — две стороны одной медали.

А где же тогда хранится «состояние»? Состояние ресурсов (данные пользователей, заказов) живёт в общей базе данных — это нормально. REST запрещает хранить именно состояние клиента/сессии в памяти конкретного сервера, а не данные вообще.

Частые ошибки

  • Путать ресурс и представление. Ресурс — абстрактная сущность с URL; представление — конкретный JSON/XML, который прислали. Один ресурс — много представлений.
  • Считать stateless запретом на базу данных. Нет: данные хранить можно и нужно. Нельзя хранить состояние клиента (сессию) в памяти сервера между запросами.
  • Думать, что REST — это протокол. REST — стиль (набор ограничений). Протокол транспорта — HTTP.
  • Использовать серверные сессии в памяти и называть API RESTful. Это нарушает stateless и мешает масштабированию.

Итог

  • REST — архитектурный стиль из шести ограничений, а не протокол.
  • Ресурс — абстрактная сущность с URL; представление — конкретный «снимок» (JSON/XML) этого ресурса.
  • Uniform Interface: со всеми ресурсами работают единообразно через URL и HTTP-методы.
  • Stateless: сервер не хранит состояние клиента; каждый запрос самодостаточен — это даёт масштабируемость и надёжность.
  • Состояние ресурсов в общей базе — это нормально; запрещены именно серверные сессии в памяти.
Проверьте себя
1. Чем ресурс отличается от представления в REST?
AЭто одно и то же, просто разные слова
BРесурс — конкретный JSON, а представление — его URL
CРесурс — абстрактная сущность с URL, а представление — конкретный формат (JSON/XML), который прислали
DРесурс хранится на клиенте, а представление — на сервере
2. Что на самом деле означает ограничение stateless?
AСерверу запрещено использовать базу данных
BСервер не хранит состояние клиента между запросами; каждый запрос самодостаточен
CКлиент не может отправлять данные в теле запроса
DВсе запросы должны быть только GET
3. Почему stateless помогает масштабировать систему?
AПотому что уменьшает размер каждого запроса
BПотому что любой из серверов за балансировщиком может обработать любой запрос — клиент не привязан к конкретному серверу
CПотому что отключает кэширование
DПотому что заставляет использовать только один сервер