Принципы REST по Филдингу: что на самом деле скрывается за аббревиатурой
Если ты junior data engineer и впервые слышишь слово REST, скорее всего ты знаешь это так: “URL вида /users/123, методы GET и POST, отдаёт JSON”. Это и правда работает, и так живут 90 процентов API в индустрии. Но настоящий REST — это не про URL и не про JSON. Это набор архитектурных ограничений, описанных Roy Fielding в его докторской диссертации в 2000 году. Имя ему — Representational State Transfer.
В этом уроке разберёмся, что значат шесть constraint-ов Филдинга, что такое resource и почему его нельзя путать с representation, и почему почти ни один популярный API в мире не соответствует REST в строгом смысле слова. Это важно не для того, чтобы быть теоретическим пуристом — а чтобы видеть архитектурные tradeoff-ы, которые скрыты за словом “RESTful” в документации.
Откуда взялся REST
В 1999 году Roy Fielding был одним из соавторов спецификации HTTP/1.1. В диссертации Architectural Styles and the Design of Network-based Software Architectures (2000) он задал вопрос: какие архитектурные свойства делают веб таким масштабируемым? Не Apache HTTP server конкретно, а сам стиль.
Ответ — шесть constraint-ов. Это не “правила” или “best practices”. Это ограничения: если ты их соблюдаешь, твоя система получает свойства, которые наблюдаются у Web — кэшируемость, устойчивость к падениям, независимая эволюция клиента и сервера, прозрачные промежуточные узлы.
REST не привязан к HTTP. Можно построить RESTful систему поверх любого транспорта (хотя HTTP — естественный кандидат). И наоборот: можно делать HTTP API, которые не RESTful.
Филдинг лично жаловался в блоге, что индустрия исказила термин REST до неузнаваемости. В заметке REST APIs must be hypertext-driven (2008) он прямо пишет: “если приложение не использует hypermedia как двигатель состояния, его нельзя называть RESTful”. По его критериям почти ни один популярный API (Stripe, GitHub, Twitter) — не REST. Они “HTTP-based RPC” или “REST-ish”.
Шесть constraint-ов
Дальше — по каждому constraint-у с примерами нарушения.
1. Client-Server
Идея: клиент и сервер — это разные компоненты с разными ответственностями. Клиент знает про UI, пользователя, взаимодействие. Сервер знает про данные, бизнес-логику, хранение. Они общаются через сеть.
Зачем: независимая эволюция. Можно переписать фронтенд на Solid вместо React, не трогая backend. Можно поменять backend с Python на Rust, не трогая клиент. Главное — стабильный контракт между ними.
Нарушение: server-side rendering, при котором сервер генерирует HTML и привязывает его к конкретному рендеру (например, серверу нужно знать про CSS-классы клиента). Это не убивает client-server — но размывает границу.
Для junior DE это очевидно: твой ETL pipeline дёргает API, парсит JSON, кладёт в Parquet. Pipeline ничего не знает про то, как API хранит данные внутри (PostgreSQL? MongoDB? in-memory?). API ничего не знает про твой Parquet.
2. Stateless
Идея: каждый запрос от клиента к серверу содержит всю информацию, нужную для его обработки. Сервер не хранит контекст между запросами. Если нужно знать, кто пользователь — токен в каждом запросе. Если нужно знать, какую страницу пагинации — offset/cursor в каждом запросе.
Зачем:
- Масштабируемость: любой запрос может уйти на любой инстанс сервера. Не нужны sticky sessions, можно балансить round-robin.
- Устойчивость: упал инстанс — клиент повторил запрос на другой, ничего не потерял.
- Простота: сервер не должен помнить миллионы открытых сессий.
Нарушение: “shopping cart на сервере” по session_id. Клиент шлёт только session_id, сервер хранит весь cart в памяти/Redis. Это работает, но это не stateless — состояние на сервере.
REST-way: cart на клиенте (или в БД, но привязанный к user_id, не session). Каждый запрос несёт Authorization: Bearer <token>, по нему сервер находит пользователя в БД и достаёт его cart.
Stateless не значит “сервер без БД”. База данных — это persistent state, она часть ресурсов. Stateless про сессионное состояние: между запросами сервер не помнит “что вы делали в предыдущем запросе”. Каждый запрос — самодостаточный.
# НЕ stateless: сервер помнит, на какой странице ты остановился
GET /api/users/next-page
# Сервер: "ага, ты уже видел страницы 1-5, вот страница 6"
# Stateless: клиент явно говорит, что хочет
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20
# Сервер: "вот 20 элементов начиная с cursor". Всё что нужно -- в URL.
3. Cacheable
Идея: ответы сервера явно помечены — кэшируемы или нет. Клиенты и промежуточные узлы могут переиспользовать кэш.
В HTTP это делается через headers: Cache-Control: max-age=3600, ETag: "abc123", Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT. На уроке про HTTP-кэширование разбирали детально.
Зачем: меньше запросов до сервера = меньше latency, меньше нагрузка, дешевле инфраструктура. CDN типа CloudFlare работает именно за счёт этого constraint-а.
Нарушение: “всегда отвечай динамически”. Сервер не выставляет Cache-Control, или выставляет no-store, даже когда данные меняются раз в день. Клиент вынужден лезть на сервер каждый раз.
Для DE: если ты дёргаешь API каждые 5 минут, а данные обновляются раз в час — выставление Cache-Control: max-age=3600 на стороне API сократит твои API-вызовы в 12 раз. Если API не выставляет — кэшируй сам.
4. Uniform Interface
Это центральный и самый сложный constraint. Он состоит из четырёх под-принципов:
4.1. Identification of resources
Каждый ресурс имеет уникальный идентификатор — URI. Не “пользователь под номером 123 в таблице users”, а /users/123. URI стабилен и не зависит от того, как сервер хранит данные внутри.
4.2. Manipulation through representations
Клиент работает с representation ресурса, не с ресурсом напрямую. Это критичное различие, разберём ниже.
4.3. Self-descriptive messages
Каждое сообщение содержит метаданные, описывающие как его обработать: Content-Type: application/json, Cache-Control, статус-коды. Клиент не должен догадываться “это JSON или XML?” — он смотрит в Content-Type.
4.4. HATEOAS (Hypermedia As The Engine Of Application State)
Самый игнорируемый под-принцип. Идея: ответ сервера содержит links на следующие действия. Клиент не должен знать URL заранее — он находит их в ответе.
{
"id": 123,
"name": "Alice",
"_links": {
"self": { "href": "/users/123" },
"orders": { "href": "/users/123/orders" },
"delete": { "href": "/users/123", "method": "DELETE" }
}
}
В реальности почти никто HATEOAS не делает. Github и Stripe — частичные исключения (Stripe возвращает next_page URL для пагинации, Github возвращает Link: <...>; rel="next" header). Поэтому Филдинг и злится: “это не REST”.
Resource vs Representation — критическое различие
Resource — это абстракция. Это сущность, на которую можно сослаться. “Список пользователей”, “пользователь с id 123”, “погода в Москве сегодня”. Resource не имеет конкретной формы, это понятие.
Representation — это конкретное представление ресурса в данный момент. JSON, XML, HTML, Protobuf. Один resource может иметь много representation.
Клиент договаривается о representation через content negotiation — header Accept:
$ curl -H "Accept: application/json" https://api.example.com/users/123
{"id": 123, "name": "Alice"}
$ curl -H "Accept: application/xml" https://api.example.com/users/123
<user><id>123</id><name>Alice</name></user>
URL /users/123 — это identifier ресурса, не файла. То, что вернётся — representation, и она может меняться от запроса к запросу (по Accept, по Accept-Language, по версии API).
Это различие — ключ к версионированию. Если меняется representation (структура JSON) — это не значит, что меняется resource. Тот же /users/123 ресурс может иметь representation v1 и v2. Через Accept: application/vnd.api+json;version=2 клиент выбирает, какую он хочет.
5. Layered System
L4 vs L7 load balancing: transport против applicationИдея: клиент не знает, общается ли он с сервером напрямую или через слои промежуточных узлов — load balancer, CDN, reverse proxy, API gateway, security layer.
Зачем: добавление CDN перед API не должно требовать изменений в клиенте. Замена inhouse load balancer на AWS ALB — то же самое.
Нарушение: клиент опирается на specific behavior сервера, который ломается при добавлении прокси. Например, держит долгую TCP connection с предположением “это тот же физический сервер” — а после load balancer-а каждый запрос может уйти на разный backend.
В жизни это про то, что хорошо спроектированный REST API должен работать одинаково независимо от inflight инфраструктуры. Если твой ETL ломается, потому что API внезапно стал отвечать через CloudFront — это плохо спроектированный pipeline.
6. Code-on-Demand (optional)
Идея: сервер может присылать исполняемый код, который клиент выполнит. Классический пример — JavaScript в HTML-странице.
Это единственный optional constraint. Можно быть RESTful, не выполняя его.
В REST API в стиле “JSON over HTTP” этот constraint обычно игнорируется. Ну и ладно.
Уровни Richardson Maturity Model
Leonard Richardson предложил модель оценки “насколько твой API близок к REST”. Четыре уровня:
| Уровень | Что добавлено | Пример |
|---|---|---|
| 0 | HTTP как транспорт для RPC | SOAP, XML-RPC. Один URL /api, всё через POST |
| 1 | Resources (URLs для сущностей) | /users, /orders/123 — но всё через POST |
| 2 | HTTP методы и статусы | GET для чтения, POST/PUT/DELETE для записи. Корректные коды 200/201/404/409 |
| 3 | Hypermedia controls (HATEOAS) | Ответы содержат links на next actions |
Большинство “REST API” в индустрии — это уровень 2. GitHub, Stripe, Slack — уровень 2 (с лёгким уклоном в 3 для пагинации). Уровень 3 — редкость.
Для junior DE: знать модель полезно, но не нужно сходить с ума. Уровень 2 — это абсолютный minimum. Если API использует один URL и всё через POST — это RPC, не REST.
Попробуй сам: оцени API по Filding-у
Возьми любой публичный API (например, GitHub, OpenWeatherMap, JSONPlaceholder) и пройдись по списку:
import requests
# JSONPlaceholder -- fake REST API для обучения
r = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(r.status_code) # 200 -- корректный код
print(r.headers["Content-Type"]) # application/json; charset=utf-8 -- self-descriptive
print(r.headers.get("Cache-Control")) # max-age=43200 -- cacheable
print(r.json()) # representation в JSON
# Клиент-сервер: yes
# Stateless: yes (нет cookies/sessions для этого ресурса)
# Cacheable: yes (Cache-Control)
# Uniform interface: частично -- есть resources, есть HTTP методы, нет HATEOAS
# Layered: yes (за CloudFront)
# Code-on-demand: no
# Вердикт: уровень 2 по Richardson
$ curl -i https://api.github.com/users/torvalds | head -20
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: public, max-age=60, s-maxage=60
etag: W/"abc..."
link: <https://api.github.com/...>; rel="next"
Github более RESTful: есть Link header (зачаток HATEOAS), ETag (conditional requests), Cache-Control.
Killer takeaway
REST — это не “URL и JSON”. Это шесть архитектурных constraint-ов из диссертации Филдинга 2000 года: client-server, stateless, cacheable, uniform interface, layered system, code-on-demand. Самый важный и самый игнорируемый — uniform interface, особенно его HATEOAS-под-принцип. Большинство API в индустрии — это уровень 2 по Richardson Maturity Model: ресурсы как URL, HTTP методы и статусы, но без hypermedia. Запомни различие: resource — абстракция, representation — конкретный JSON/XML/HTML, который сервер вернул. URL /users/123 идентифицирует ресурс, а не файл.