Learning Platform
Глоссарий Troubleshooting
Урок 06.01 · 25 мин
Начальный
restfieldingarchitectureconstraintsresourcerepresentation

Принципы 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.

NOTE

Филдинг лично жаловался в блоге, что индустрия исказила термин REST до неузнаваемости. В заметке REST APIs must be hypertext-driven (2008) он прямо пишет: “если приложение не использует hypermedia как двигатель состояния, его нельзя называть RESTful”. По его критериям почти ни один популярный API (Stripe, GitHub, Twitter) — не REST. Они “HTTP-based RPC” или “REST-ish”.


Шесть constraint-ов

Constraint-ы REST накапливаются от нулевого стиля к полному REST
Null style (нет ограничений)Стартовая точка -- никаких ограничений. Любая распределённая система. Например, монолит без сети
+ Client-ServerClient-Server: разделение ответственности -- клиент про UI и взаимодействие с пользователем, сервер про данные и логику. Они эволюционируют независимо
+ StatelessStateless: каждый запрос содержит всё для его обработки, сервер не помнит предыдущие. Сессионное состояние -- на клиенте
+ CacheableCacheable: ответы помечены -- можно кэшировать или нет. Промежуточные кэши снижают нагрузку и latency
+ Uniform InterfaceUniform Interface: единообразный интерфейс ресурсов. Resource identification, manipulation through representations, self-descriptive messages, HATEOAS
+ Layered SystemLayered System: клиент не знает, общается ли он с сервером напрямую или через прокси/CDN/load balancer
+ Code-on-Demand (optional)Code-on-Demand: сервер может присылать исполняемый код (опциональный constraint). Например, JavaScript в HTML

Дальше — по каждому 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.

WARNING

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
Resource: /users/123Абстракция: пользователь Alice с её данными. Resource не имеет формата, это понятие
Accept: application/json. Сервер вернёт JSON-объект
Accept: application/xml. Сервер вернёт XML-документ
Accept: text/html. Сервер вернёт HTML-страницу с профилем
Accept: application/vnd.ms-excel. Сервер вернёт Excel-файл

Клиент договаривается о 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).

TIP

Это различие — ключ к версионированию. Если меняется 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”. Четыре уровня:

УровеньЧто добавленоПример
0HTTP как транспорт для RPCSOAP, XML-RPC. Один URL /api, всё через POST
1Resources (URLs для сущностей)/users, /orders/123 — но всё через POST
2HTTP методы и статусыGET для чтения, POST/PUT/DELETE для записи. Корректные коды 200/201/404/409
3Hypermedia 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 идентифицирует ресурс, а не файл.

Проверка знанийKnowledge check
ОтветAnswer

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Кто и в какой работе впервые формально описал архитектурные constraint-ы REST?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4