Ресурсы
Ресурсы в REST — всё, с чем работает API (пользователи, заказы), идентифицируется URI-существительным (/users/123), а действия — HTTP-методами. Ресурс отделён от представления (JSON/XML/HTML), коллекции и вложенные ресурсы — тоже ресурсы, глубокая вложенность не нужна.
Введение: Что такое ресурс в REST
В предыдущей теме мы говорили о принципах REST. Один из ключевых принципов — единообразие интерфейса. А сердце этого единообразия — ресурс.
Представьте, что вы работаете в библиотеке. Библиотека — это система. Что в ней есть? Книги, читатели, авторы, жанры, полки. Это всё — ресурсы. Каждый ресурс имеет своё уникальное место в системе: книга с определённым инвентарным номером, читатель с номером читательского билета.
В REST API всё строится вокруг ресурсов. Пользователь — это ресурс. Заказ — это ресурс. Товар — это ресурс. Фотография — это ресурс. Даже коллекция пользователей — это ресурс.
Ресурс (Resource) — это любой объект, информацию о котором можно получить, создать, изменить или удалить. У каждого ресурса есть уникальный идентификатор — URI (Uniform Resource Identifier).
Клиент не вызывает “функции” или “процедуры” на сервере. Он работает с ресурсами: берёт их, кладёт, заменяет, удаляет. Это фундаментальный сдвиг в мышлении по сравнению с традиционными RPC-подходами.
Что может быть ресурсом
Ресурсом может быть что угодно, что имеет смысл в вашем приложении.
| Тип ресурса | Примеры | URI пример |
|---|---|---|
| Отдельный объект | Пользователь, заказ, товар, статья | /users/123, /orders/456 |
| Коллекция | Список пользователей, все заказы | /users, /orders |
| Вложенный ресурс | Заказы пользователя, товары в заказе | /users/123/orders, /orders/456/items |
| Файл | Изображение, документ, видео | /images/photo.jpg, /documents/report.pdf |
| Служебный ресурс | Статус, настройки, метрики | /health, /config, /metrics |
Идентификация ресурсов: URI
У каждого ресурса должен быть уникальный идентификатор — URI. URI — это адрес ресурса в системе.
Хорошие URI
/users/123
/users/123/orders
/orders/456
/orders/456/items
/products?category=electronics
/search?q=iphoneПлохие URI
/getUser?id=123
/saveUser
/deleteUser
/user.php?id=123
/api/v1/getOrder?order_id=456Почему плохие URI плохие:
| Проблема | Объяснение |
|---|---|
| Глагол в URI | URI должен идентифицировать ресурс, а не действие. Действие — в HTTP методе |
| Разные форматы | /getUser, /saveUser, /deleteUser — нет единого паттерна |
| Расширения файлов | .php, .aspx — привязывают к технологии |
| Использование query параметров для идентификации | ?id=123 — идентификатор должен быть в пути, не в параметрах |
Именование ресурсов
Используйте существительные, не глаголы
# Плохо
/getUsers
/createUser
/updateUser
/deleteUser
# Хорошо
GET /users
POST /users
PUT /users/123
DELETE /users/123Используйте множественное число для коллекций
# Плохо
/user # неясно, один пользователь или коллекция
/user/list # избыточно
# Хорошо
/users # коллекция пользователей
/users/123 # конкретный пользовательИспользуйте вложенность для отношений
# Заказы пользователя
/users/123/orders
# Товары в заказе
/orders/456/items
# Комментарии к статье
/articles/789/commentsИзбегайте глубокой вложенности
# Плохо (слишком глубоко)
/users/123/orders/456/items/789/shipments/101
# Хорошо (плоская структура)
/orders/456/items/789/shipments/101
# Или
/shipments/101?order_id=456&item_id=789Представление ресурсов
Клиент и сервер обмениваются не самими ресурсами, а их представлениями (representations). Один ресурс может иметь несколько представлений.
Пример: Пользователь как ресурс
Ресурс “пользователь” может быть представлен в разных форматах:
JSON представление (для API):
{
"id": 123,
"name": "Иван Петров",
"email": "ivan@example.com",
"created_at": "2024-01-15T10:30:00Z"
}HTML представление (для браузера):
<div class="user">
<h1>Иван Петров</h1>
<p>Email: ivan@example.com</p>
<p>Зарегистрирован: 15.01.2024</p>
</div>XML представление (для старых систем):
<user>
<id>123</id>
<name>Иван Петров</name>
<email>ivan@example.com</email>
<created_at>2024-01-15T10:30:00Z</created_at>
</user>Content Negotiation (Согласование содержимого)
Клиент указывает, какое представление он хочет получить, с помощью заголовка Accept.
GET /users/123
Accept: application/jsonGET /users/123
Accept: application/xmlGET /users/123
Accept: text/htmlСервер отвечает с соответствующим Content-Type:
HTTP/1.1 200 OK
Content-Type: application/jsonНеправильный подход: Расширение в URI
# Плохо (формат в URI)
GET /users/123.json
GET /users/123.xml
# Хорошо (content negotiation)
GET /users/123
Accept: application/jsonПочему content negotiation лучше: URI идентифицирует ресурс, а не его представление. Один ресурс — один URI.
Коллекции как ресурсы
Коллекция — это ресурс, который содержит другие ресурсы.
GET /users # коллекция пользователей
POST /users # создать нового пользователя в коллекции
GET /users?status=active # отфильтрованная коллекцияОтвет для коллекции
{
"data": [
{"id": 1, "name": "Иван"},
{"id": 2, "name": "Петр"},
{"id": 3, "name": "Анна"}
],
"pagination": {
"total": 100,
"limit": 10,
"offset": 0,
"next": "/users?limit=10&offset=10"
}
}Вложенные ресурсы
Вложенные ресурсы выражают отношения между ресурсами.
GET /users/123/orders # заказы пользователя 123
GET /users/123/orders/456 # заказ 456 пользователя 123
POST /users/123/orders # создать заказ для пользователя 123Когда использовать вложенность
| Связь | Вложенность | Пример |
|---|---|---|
| Владение (ownership) | Да | /users/123/orders — заказы принадлежат пользователю |
| Содержание (containment) | Да | /orders/456/items — товары содержатся в заказе |
| Ссылка (reference) | Нет | /orders/456?customer_id=123 — заказ ссылается на пользователя |
Свойства vs Ресурсы
Иногда возникает вопрос: сделать что-то свойством ресурса или отдельным ресурсом?
{
"user": {
"id": 123,
"name": "Иван",
"address": { // address как свойство
"city": "Москва",
"street": "Тверская"
}
}
}// address как отдельный ресурс
{
"user": {
"id": 123,
"name": "Иван",
"address_id": 456
}
}
GET /addresses/456Когда что выбирать:
| Критерий | Свойство | Отдельный ресурс |
|---|---|---|
| Размер | Маленький | Большой |
| Самостоятельность | Не имеет смысла без родителя | Имеет смысл сам по себе |
| Частота изменений | Меняется вместе с родителем | Меняется независимо |
| Доступ | Всегда с родителем | Может быть запрошен отдельно |
| Повторное использование | Не используется другими ресурсами | Используется несколькими ресурсами |
Пустые ресурсы
Иногда ресурс может существовать, даже если у него нет данных. Например, настройки пользователя по умолчанию.
GET /users/123/settings{
"theme": "light", // есть значение по умолчанию
"notifications": true,
"language": "ru"
}Даже если пользователь никогда не менял настройки, ресурс существует и возвращает значения по умолчанию.
Виртуальные ресурсы
Не все ресурсы должны соответствовать объектам в базе данных. Ресурс может быть “виртуальным” — вычисляемым на лету.
GET /users/123/stats{
"total_orders": 42,
"total_spent": 125000,
"avg_order": 2976,
"last_order_date": "2024-03-15",
"favorite_category": "electronics"
}Эти данные не хранятся как отдельный ресурс, а вычисляются из других данных при запросе.
Служебные ресурсы
Некоторые ресурсы нужны для управления самой системой, а не для бизнес-логики.
GET /health # состояние сервиса
GET /metrics # метрики (прометеус)
GET /config # текущая конфигурация
GET /version # версия API
GET /docs # документация
GET /status # общий статус системыПримеры проектирования ресурсов
Пример 1: Блог
# Статьи
GET /articles
GET /articles/123
POST /articles
PUT /articles/123
DELETE /articles/123
# Комментарии к статье (вложенные)
GET /articles/123/comments
POST /articles/123/comments
GET /comments/456
DELETE /comments/456
# Авторы
GET /authors
GET /authors/456
GET /authors/456/articles
# Теги
GET /tags
GET /tags/789/articlesПример 2: Интернет-магазин
# Товары
GET /products
GET /products/123
GET /products?category=electronics&sort=price_asc
# Категории
GET /categories
GET /categories/456/products
# Корзина (принадлежит пользователю)
GET /users/789/cart
POST /users/789/cart/items
DELETE /users/789/cart/items/101
PATCH /users/789/cart/items/101
# Заказы
GET /users/789/orders
POST /users/789/orders
GET /orders/456
GET /orders/456/itemsПример 3: Система бронирования
# Ресурсы для бронирования
GET /rooms
GET /rooms/123
GET /rooms?available=true&date=2024-12-25
# Бронирования
GET /bookings
GET /bookings/456
POST /bookings
DELETE /bookings/456
# Бронирования конкретного пользователя
GET /users/789/bookings
# Проверка доступности (виртуальный ресурс)
GET /rooms/123/availability?from=2024-12-25&to=2025-01-05Проблемы при проектировании ресурсов
Проблема 1: Слишком много ресурсов
Каждое свойство — отдельный ресурс:
GET /users/123/name
GET /users/123/email
GET /users/123/phoneЛучше: Один ресурс /users/123 со всеми свойствами.
Проблема 2: Слишком мало ресурсов
Огромный ресурс, содержащий всё:
GET /everythingЛучше: Разделить на логические ресурсы.
Проблема 3: Неоднозначные URI
GET /user/123 # или /users/123?
GET /getUser/123 # или /user/123?
GET /user?id=123 # или /users/123?
GET /users/123/get # или /users/123?Лучше: Единый стиль. /users/123
Проблема 4: URI с действиями
POST /users/123/activate
POST /users/123/deactivate
POST /users/123/blockЛучше: Использовать PATCH для изменения статуса или вложенный ресурс состояния.
PATCH /users/123
{"status": "active"}
# или
GET /users/123/status
PUT /users/123/status
{"status": "active"}Ресурсы и состояние приложения
В REST состояние приложения хранится на клиенте. Сервер не помнит, где клиент находится.
Пример: Оформление заказа в несколько шагов.
Не-REST (с состоянием на сервере):
POST /checkout/step1 # сервер запоминает шаг 1
POST /checkout/step2 # сервер вспоминает шаг 1
POST /checkout/step3 # сервер вспоминает шаги 1 и 2REST (состояние на клиенте):
POST /orders
{"items": [...], "shipping_address": {...}, "payment": {...}}
# Вся информация в одном запросеЕсли процесс слишком сложный для одного запроса, клиент собирает данные постепенно, но хранит их у себя, а серверу отправляет только финальный результат.
Резюме для системного аналитика
Ресурс — это центральное понятие REST. Всё, с чем работает API, — это ресурсы. Пользователь, заказ, товар, коллекция, файл.
Каждый ресурс идентифицируется уникальным URI. URI должен быть существительным, не глаголом. Действие — в HTTP методе.
Ресурс отделён от его представления. Один ресурс может иметь несколько представлений (JSON, XML, HTML). Клиент выбирает представление через
Acceptзаголовок.Коллекции — это тоже ресурсы.
/users— ресурс коллекции./users/123— ресурс элемента.Вложенность выражает отношения.
/users/123/orders— заказы принадлежат пользователю. Но избегайте глубокой вложенности (больше 3 уровней).Не всё должно быть ресурсом. Мелкие свойства могут оставаться свойствами. Вычисляемые данные могут быть виртуальными ресурсами.