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

Ресурсы

Ресурсы в 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 плохие:

ПроблемаОбъяснение
Глагол в URIURI должен идентифицировать ресурс, а не действие. Действие — в 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/json
GET /users/123
Accept: application/xml
GET /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 и 2

REST (состояние на клиенте):

POST /orders
{"items": [...], "shipping_address": {...}, "payment": {...}}
# Вся информация в одном запросе

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

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

  1. Ресурс — это центральное понятие REST. Всё, с чем работает API, — это ресурсы. Пользователь, заказ, товар, коллекция, файл.

  2. Каждый ресурс идентифицируется уникальным URI. URI должен быть существительным, не глаголом. Действие — в HTTP методе.

  3. Ресурс отделён от его представления. Один ресурс может иметь несколько представлений (JSON, XML, HTML). Клиент выбирает представление через Accept заголовок.

  4. Коллекции — это тоже ресурсы. /users — ресурс коллекции. /users/123 — ресурс элемента.

  5. Вложенность выражает отношения. /users/123/orders — заказы принадлежат пользователю. Но избегайте глубокой вложенности (больше 3 уровней).

  6. Не всё должно быть ресурсом. Мелкие свойства могут оставаться свойствами. Вычисляемые данные могут быть виртуальными ресурсами.

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

Вопрос 1 из 4
Что является центральным понятием REST?
Как обычно правильно именуют ресурсы в URL?
Что показывает URL ресурса?
Почему слишком глубокие URL часто считаются плохой практикой?

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