Перейти к содержимому

Принципы REST

REST — архитектурный стиль (не протокол) для веб-сервисов. Шесть принципов: клиент-сервер (независимое развитие), stateless (каждый запрос самодостаточен, сервер не хранит сессии), кеширование (Cache-Control, ETag), единообразие интерфейса (ресурсы через URI, действия через HTTP-методы, HATEOAS — опционально), слоистая система (клиент не знает о промежуточных слоях), код по требованию (опционально). Ключевое отличие REST от других подходов — stateless (горизонтальное масштабирование, любой сервер обрабатывает любой запрос). RESTful API: ресурсы в URI (существительные), методы GET/POST/PUT/PATCH/DELETE по назначению, stateless, правильные HTTP статусы (200, 201, 204, 400, 404), явное кеширование. Типичные ошибки: GET для изменения данных, хранение сессий на сервере, GET с телом, всегда 200 OK с ошибкой в теле, отсутствие кеширования.

Введение: Архитектура для веба

Представьте, что вы проектируете библиотеку. Можно сделать так, чтобы читатель подходил к библиотекарю и говорил: “Дай мне книгу”. Библиотекарь идет на склад, находит книгу, приносит. Это работает, но библиотекарь — узкое место. Каждый запрос требует его участия.

А можно сделать иначе. Расставить книги на открытых полках. Сделать каталог, где каждая книга имеет свой адрес — ряд, полка, место. Тогда читатель сам идет к нужной полке и берет книгу. Библиотекарь нужен только для сложных операций.

REST (Representational State Transfer) — это архитектурный стиль, предложенный Роем Филдингом в его диссертации в 2000 году. Он описывает, как должны быть устроены веб-сервисы, чтобы быть масштабируемыми, надежными и простыми.

REST не протокол и не стандарт. Это набор принципов и ограничений. Веб-сервисы, которые следуют этим принципам, называют RESTful. HTTP — это реализация принципов REST в протоколе. Именно поэтому REST так хорошо сочетается с вебом.

Принципы REST определяют, как должны быть организованы взаимодействия между клиентом и сервером в распределенных системах, особенно в вебе.

Шесть принципов REST

Рой Филдинг определил шесть ограничений (принципов), которые характеризуют REST:

ПринципСуть
1. Клиент-серверная архитектураРазделение ответственности между клиентом и сервером
2. Отсутствие состояния (Stateless)Сервер не хранит контекст между запросами клиента
3. КешированиеОтветы должны быть помечены как кешируемые или нет
4. Единообразие интерфейсаЕдиные правила для всех взаимодействий
5. Слоистая системаКлиент не знает, напрямую ли он общается с сервером или через промежуточные слои
6. Код по требованию (опционально)Сервер может передавать клиенту исполняемый код

1. Клиент-серверная архитектура

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

    graph LR
    A[Клиент<br>интерфейс] -->|Запрос| B[Сервер<br>данные и логика]
    B -->|Ответ| A
  

Преимущества разделения:

ПреимуществоОбъяснение
Независимое развитиеКоманда клиента может менять интерфейс, не спрашивая серверную команду. И наоборот
ПортативностьОдин сервер могут использовать разные клиенты: веб, мобильное приложение, десктоп, другой сервер
МасштабированиеКлиенты и серверы можно масштабировать независимо
ПростотаКаждый компонент занимается своим делом

Нарушение принципа

<!-- Плохо: сервер возвращает готовый HTML с логикой -->
<button onclick="submitForm()">Отправить</button>
<script>
    // Бизнес-логика на сервере, смешанная с представлением
    function submitForm() {
        fetch('/save', {...});
    }
</script>

Следование принципу

// Хорошо: сервер возвращает чистые данные
{
    "id": 123,
    "name": "Иван",
    "email": "ivan@example.com"
}

// Клиент сам решает, как их отобразить

2. Отсутствие состояния (Stateless)

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

Аналогия

Сервер с состоянием (stateful): Вы заходите в магазин, продавец запоминает вас. В следующий раз он говорит: “А, это вы, хотите то же, что и в прошлый раз?” Если продавец сменился — он ничего о вас не знает.

Сервер без состояния (stateless): Вы заходите в магазин и каждый раз говорите: “Дайте мне хлеб, масло и молоко”. Продавец не помнит вас, не помнит ваши предыдущие покупки. Он просто выполняет ваш текущий запрос.

Почему это важно

ПреимуществоОбъяснение
МасштабируемостьЛюбой сервер в кластере может обработать любой запрос. Не нужно “привязывать” клиента к конкретному серверу
НадежностьЕсли сервер упал, другой сервер может продолжить обработку запросов. Нет потерянного состояния
ПростотаСерверу не нужно управлять сессиями, синхронизировать состояние между экземплярами
КешированиеОтветы легче кешировать, когда они не зависят от предыдущих запросов

Пример: Авторизация

Сервер с состоянием (не RESTful):

POST /login
Content-Type: application/json

{"username": "ivan", "password": "123"}
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123
GET /profile
Cookie: session_id=abc123

Сервер хранит на своей стороне, что session_id=abc123 принадлежит Ивану.

Сервер без состояния (RESTful):

POST /login
Content-Type: application/json

{"username": "ivan", "password": "123"}
HTTP/1.1 200 OK
Content-Type: application/json

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
GET /profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Сервер не хранит сессию. Токен содержит всю информацию о пользователе. Любой сервер может проверить токен и понять, кто делает запрос.

Что делать с сессиями

В RESTful API сессии не хранятся на сервере. Вместо этого используются:

МеханизмОписание
JWT (JSON Web Token)Токен, содержащий информацию о пользователе. Сервер его подписывает, но не хранит
API KeyКлюч, который клиент передает в каждом запросе
OAuth 2.0Токен доступа, который передается в заголовке

Нарушение принципа

# Плохо: сервер хранит состояние
sessions = {}

@app.post('/login')
def login():
    session_id = generate_id()
    sessions[session_id] = {'user': 'ivan'}
    return {'session_id': session_id}

@app.get('/profile')
def profile():
    session = sessions.get(request.cookies['session_id'])
    return {'name': session['user']}  # зависит от предыдущего запроса

Следование принципу

# Хорошо: каждый запрос самодостаточен
@app.post('/login')
def login():
    user = authenticate(request.json)
    token = jwt.encode({'user': user.name}, SECRET)
    return {'token': token}

@app.get('/profile')
def profile():
    token = request.headers['Authorization']
    user = jwt.decode(token, SECRET)
    return {'name': user['name']}  # вся информация в запросе

3. Кеширование

Ответы сервера должны явно указывать, можно ли их кешировать и на сколько. Кеширование может происходить на клиенте, на промежуточных прокси, на CDN.

Почему это важно

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

HTTP заголовки для кеширования

ЗаголовокНазначение
Cache-Control: max-age=3600Кешировать на 1 час
Cache-Control: no-cacheПроверять свежесть при каждом запросе
Cache-Control: no-storeНе кешировать вообще
ETag: "abc123"Хеш содержимого. Клиент может отправить If-None-Match
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMTДата последнего изменения

Пример с ETag

GET /users/123
HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600

{"id": 123, "name": "Иван"}

При повторном запросе клиент отправляет:

GET /users/123
If-None-Match: "abc123"

Если данные не изменились:

HTTP/1.1 304 Not Modified

Нарушение принципа

HTTP/1.1 200 OK
Content-Type: application/json

<!-- Нет заголовков кеширования — неясно, можно ли кешировать -->
{"id": 123, "name": "Иван"}

Следование принципу

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=300
ETag: "d3b07384d113edec49eaa6238ad5ff00"

{"id": 123, "name": "Иван"}

4. Единообразие интерфейса

Все взаимодействия должны следовать единым правилам. Это самый сложный и самый важный принцип REST. Он требует:

ТребованиеЧто значит
Идентификация ресурсовКаждый ресурс имеет уникальный идентификатор (URI)
Представление ресурсовКлиент работает с представлением ресурса, не с самим ресурсом
Самодостаточные сообщенияСообщение содержит всю информацию для его обработки
Гипермедиа как движок состояния (HATEOAS)Сервер говорит клиенту, какие действия возможны дальше

Идентификация ресурсов

Каждый ресурс имеет свой URI (унифицированный идентификатор ресурса).

GET /users/123         # один пользователь
GET /users             # коллекция пользователей
GET /users/123/orders  # заказы пользователя
GET /orders/456        # один заказ

Представление ресурсов

Клиент и сервер обмениваются не самими ресурсами, а их представлениями. Один ресурс может иметь разные представления (JSON, XML, HTML).

GET /users/123
Accept: application/json   # хочу JSON
GET /users/123
Accept: application/xml    # хочу XML

HATEOAS (Hypermedia as the Engine of Application State)

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

{
    "id": 123,
    "name": "Иван",
    "links": [
        {"rel": "self", "href": "/users/123"},
        {"rel": "orders", "href": "/users/123/orders"},
        {"rel": "update", "href": "/users/123", "method": "PUT"},
        {"rel": "delete", "href": "/users/123", "method": "DELETE"}
    ]
}

Что это дает: Клиенту не нужно знать заранее все эндпоинты. Он может “гулять” по API, следуя ссылкам, как человек по веб-сайту.

Почему HATEOAS редко используется: Усложняет разработку клиента. Большинство публичных API не реализуют HATEOAS полностью, но это считается “чистым” REST.

Нарушение единообразия

GET /getUser?id=123
POST /saveUser
POST /deleteUser

Проблемы:

  • Разные схемы именования
  • Действие в имени, а не в методе HTTP
  • Нет единого паттерна

Следование единообразию

GET /users/123
PUT /users/123
DELETE /users/123

5. Слоистая система

Что это значит

Архитектура может состоять из нескольких слоев (layers). Клиент не должен знать, напрямую ли он общается с сервером или через промежуточные слои (прокси, шлюзы, балансировщики, CDN).

    graph TD
    A[Клиент] --> B[Балансировщик]
    B --> C[Кеш/CDN]
    C --> D[Веб-сервер]
    D --> E[Сервер приложений]
    E --> F[База данных]
  

Почему это важно

ПреимуществоОбъяснение
БезопасностьМожно добавить шлюз, проверяющий авторизацию, не меняя клиент
МасштабированиеБалансировщик распределяет нагрузку между серверами
ПроизводительностьCDN и кеширующие прокси ускоряют ответы
ИзоляцияКлиент не зависит от внутренней структуры сервера

Пример

Клиент обращается к https://api.example.com/users/123. За этим URL может стоять:

  • Балансировщик нагрузки (например, AWS ELB)
  • CDN (например, CloudFront)
  • Шлюз API (проверка ключей, rate limiting)
  • Несколько серверов приложений
  • Кеш (Redis)
  • База данных

Клиент ничего об этом не знает. И это хорошо.

6. Код по требованию (опциональный)

Что это значит

Сервер может временно расширять возможности клиента, передавая ему исполняемый код (например, JavaScript).

Пример

GET /calculator
<!-- Сервер возвращает HTML с JavaScript -->
<script>
    function calculate() {
        // Код, который выполнится на клиенте
        return price * quantity;
    }
</script>

Почему это опционально

Этот принцип не обязателен. REST архитектура может быть и без него. Многие RESTful API не используют его.

REST и HTTP: В чем разница

REST — это архитектурный стиль. HTTP — это протокол, который может реализовывать принципы REST.

АспектRESTHTTP
СтатусАрхитектурный стильПротокол
УровеньКонцептуальныйТехнический
ПринципыКлиент-сервер, stateless, кеширование, единообразиеМетоды, заголовки, статус-коды
ОбязательностьРекомендацииСпецификация

Важно: Не каждый HTTP API является RESTful. API на HTTP может нарушать принципы REST (например, хранить состояние на сервере). И наоборот — REST может быть реализован не только на HTTP (хотя это редкость).

RESTful vs не-RESTful

ХарактеристикаRESTfulНе-RESTful
URLРесурсы: /users/123Действия: /getUser?id=123
МетодыGET, POST, PUT, DELETE, PATCHGET, POST (иногда только POST)
СостояниеStateless (каждый запрос самодостаточен)Stateful (сессии на сервере)
КешированиеЯвное (заголовки Cache-Control)Часто отсутствует
ОтветыПонятные статус-кодыЧасто всегда 200 с ошибкой в теле

Пример: Не-RESTful API

POST /api/getUser
Content-Type: application/json

{"user_id": 123}
HTTP/1.1 200 OK
{"status": "success", "data": {"name": "Иван"}}

Проблемы:

  • POST для получения данных (должен быть GET)
  • ID в теле, не в URL
  • Не используются статус-коды (всегда 200)

Пример: RESTful API

GET /users/123
HTTP/1.1 200 OK
Cache-Control: max-age=300

{"id": 123, "name": "Иван"}

Частичное нарушение принципов

Иногда принципы REST нарушают осознанно. Например:

  • Stateful API может быть оправдан для потоковой передачи данных (WebSocket)
  • Нестандартные методы могут использоваться для сложных операций, не вписывающихся в CRUD
  • Отсутствие HATEOAS — норма для большинства современных API (это трудно реализовать и использовать)

Важно: Понимать, какой принцип нарушается и почему. Осознанное нарушение лучше неосознанного.

Распространенные ошибки

Ошибка 1: Использование GET для изменения данных

GET /api/deleteUser?id=123

Почему плохо: GET должен быть идемпотентным и безопасным (не изменять данные). Поисковые роботы, префетчеры могут случайно вызвать такой URL.

Ошибка 2: Хранение состояния на сервере

POST /api/login
→ сервер создает сессию
GET /api/profile
→ сервер использует сессию из cookie

Почему плохо: Нарушает принцип stateless. Серверы в кластере должны синхронизировать сессии.

Ошибка 3: GET с телом запроса

GET /api/search
Content-Type: application/json

{"query": "iphone", "limit": 10}

Почему плохо: GET по спецификации не должен иметь тела. Не все прокси и серверы это поддерживают.

Ошибка 4: Всегда возвращать 200 OK

HTTP/1.1 200 OK
{"error": "user not found"}

Почему плохо: Клиент не может отличить успех от ошибки по статус-коду. Приходится парсить тело ответа.

Ошибка 5: Игнорирование кеширования

Никаких заголовков Cache-Control, ETag, Last-Modified. Каждый запрос идет к серверу.

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

Резюме для системного аналитика

  1. REST — это архитектурный стиль, а не протокол. Он описывает принципы построения распределенных систем, особенно веб-сервисов.

  2. Шесть принципов REST: клиент-сервер, отсутствие состояния (stateless), кеширование, единообразие интерфейса, слоеная система, код по требованию (опционально).

  3. Stateless — ключевое отличие REST от многих других подходов. Сервер не хранит состояние клиента между запросами. Каждый запрос самодостаточен. Это позволяет горизонтально масштабировать систему.

  4. Единообразие интерфейса требует, чтобы ресурсы имели URI, клиент работал с представлениями ресурсов, а сообщения были самодостаточны. HATEOAS — самая сложная часть.

  5. REST хорошо сочетается с HTTP, но не тождественен ему. HTTP API не становится автоматически RESTful.

  6. RESTful API: ресурсы в URI, методы HTTP по назначению, stateless, правильные статус-коды, явное кеширование.

Проверка знаний

Вопрос 1 из 4
Какой принцип REST требует, чтобы каждый запрос был самодостаточным?
Что означает принцип client-server в REST?
Зачем принцип cacheable важен?
Что нарушает дух REST?

Вопросы, где были ошибки