Принципы 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=abc123GET /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/123HTTP/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 # хочу JSONGET /users/123
Accept: application/xml # хочу XMLHATEOAS (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/1235. Слоистая система
Что это значит
Архитектура может состоять из нескольких слоев (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.
| Аспект | REST | HTTP |
|---|---|---|
| Статус | Архитектурный стиль | Протокол |
| Уровень | Концептуальный | Технический |
| Принципы | Клиент-сервер, stateless, кеширование, единообразие | Методы, заголовки, статус-коды |
| Обязательность | Рекомендации | Спецификация |
Важно: Не каждый HTTP API является RESTful. API на HTTP может нарушать принципы REST (например, хранить состояние на сервере). И наоборот — REST может быть реализован не только на HTTP (хотя это редкость).
RESTful vs не-RESTful
| Характеристика | RESTful | Не-RESTful |
|---|---|---|
| URL | Ресурсы: /users/123 | Действия: /getUser?id=123 |
| Методы | GET, POST, PUT, DELETE, PATCH | GET, 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/123HTTP/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. Каждый запрос идет к серверу.
Почему плохо: Потеря производительности, избыточная нагрузка.
Резюме для системного аналитика
REST — это архитектурный стиль, а не протокол. Он описывает принципы построения распределенных систем, особенно веб-сервисов.
Шесть принципов REST: клиент-сервер, отсутствие состояния (stateless), кеширование, единообразие интерфейса, слоеная система, код по требованию (опционально).
Stateless — ключевое отличие REST от многих других подходов. Сервер не хранит состояние клиента между запросами. Каждый запрос самодостаточен. Это позволяет горизонтально масштабировать систему.
Единообразие интерфейса требует, чтобы ресурсы имели URI, клиент работал с представлениями ресурсов, а сообщения были самодостаточны. HATEOAS — самая сложная часть.
REST хорошо сочетается с HTTP, но не тождественен ему. HTTP API не становится автоматически RESTful.
RESTful API: ресурсы в URI, методы HTTP по назначению, stateless, правильные статус-коды, явное кеширование.