JavaRush /Курсы /Spring REST & MVC /REST: ресурс, коллек...

REST: ресурс, коллекция, представление

Spring REST & MVC
3 уровень , 1 лекция
Открыта

1. REST‑словарь и общий язык

Если вы хоть раз спорили в команде, “как правильно сделать REST”, то знаете эту боль: один человек говорит “ресурс” и имеет в виду JSON‑объект, другой — “табличку в базе”, третий — “endpoint”, а четвёртый — “любую сущность, у которой есть id”. Разговор получается как диалог двух микросервисов без общего контракта: все что‑то шлют, но никто никого не понимает.

В этой лекции мы фиксируем минимальный набор терминов, который позволит нам дальше проектировать Task Tracker API осознанно. Нам важно различать что существует в предметной области (ресурс), как это существует в API как набор (коллекция), как это показывается и принимается в обмене (представление) и что живёт только в контексте родителя (подресурс). Никакой философии, только инженерная ясность.

Чтобы было проще, держите в голове одну мысль: REST‑словарь нужен не ради “правильности”, а чтобы потом быстро отвечать на вопросы вроде “это отдельная сущность или часть другой?” и “этот JSON — это ресурс или просто представление?”.

Ресурс: смысл и идентичность

Когда слово “ресурс” слышишь впервые, очень хочется мысленно подставить “объект в коде” или “запись в базе”. Это естественно: мы привыкли мыслить тем, что можно потрогать. Но в REST ресурс — это прежде всего значимая часть предметной области, с которой работает клиент, и у неё есть устойчивая идентичность (обычно — идентификатор).

Ключевое здесь вот что: ресурс — это “вещь” на уровне смысла. В нашем проекте “задача” — ресурс не потому, что у нас будет класс Task (он будет), и не потому, что будет таблица tasks (её пока не будет), а потому, что клиент мыслит системой так: “вот есть задачи, я хочу посмотреть одну, изменить одну, удалить одну”. То есть для клиента “задача” — отдельная адресуемая сущность.

Очень полезный здравый критерий: ресурс — это то, на что вы можете сослаться фразой “вот эта штука” и не потерять смысл. “Эта задача”, “этот комментарий”, “это вложение”. А вот “сортировка по приоритету” — не ресурс, это параметр работы с коллекцией. “Статус” — не отдельный ресурс в нашем проекте, а часть состояния задачи (по крайней мере на текущем масштабе).

Мини‑пример на Java (не про Spring, просто про смысл “есть идентичность”):

import java.util.UUID;

public class IdExample {
    public static void main(String[] args) {
        // UUID здесь играет роль устойчивой идентичности ресурса (taskId),
        // а не "просто строки" (важно именно то, что это адресуемый идентификатор).
        String taskId = UUID.fromString("2f1d1b7a-3c8a-4f2b-9f2a-0e6f3c8b2d11").toString();

        // Клиенту важно, что по этому id можно сослаться на "ту самую" задачу.
        System.out.println(taskId); // 2f1d1b7a-3c8a-4f2b-9f2a-0e6f3c8b2d11
    }
}

Здесь не важно, где хранится задача. Важно, что ресурс “Task” в нашем проекте будет адресуемым: у него есть id (UUID string по ТЗ проекта), и это позволяет клиенту сказать: “дай мне задачу с таким id”.

2. Коллекция и отдельный ресурс

Когда мы говорим “задачи” (tasks), речь обычно идёт о двух разных типах запросов клиента, и по смыслу они сильно отличаются. Первый — “покажи мне список задач” (коллекция), второй — “покажи мне конкретную задачу” (один ресурс). Если не различать эти случаи, API начинает путаться: мы случайно пытаемся применить к одному объекту правила коллекции, а к списку — правила единичного ресурса.

Коллекция — это набор однотипных ресурсов. Важный момент: коллекция — это не просто List<Task> в Java. Коллекция в REST‑мышлении — это часть внешнего контракта: клиент может “просматривать” набор, добавлять в него элементы, искать в нём нужный. Даже если внутри мы храним данные как угодно (массив, карта, файлик, “а у нас пока просто хардкод, потому что учебный проект”), для клиента это всё равно коллекция задач.

Отдельный ресурс — это конкретный элемент коллекции. И тут уже включаются другие ожидания: у элемента есть конкретный id, у него есть состояние, и операции над ним обычно происходят “по адресу”.

Пока не обсуждаем правила “как именно должны выглядеть URI” (это будет завтра), просто зафиксируем: это два разных объекта разговора.

import java.util.List;

public class CollectionVsItemExample {
    public static void main(String[] args) {
        // Путь к коллекции: здесь мы "говорим о множестве" (список/поиск/добавление).
        String tasksCollection = "/tasks";

        // Путь к элементу: здесь мы "говорим о конкретной штуке" по её идентификатору.
        String oneTask = "/tasks/{taskId}";

        List<String> forms = List.of(tasksCollection, oneTask);

        // Наглядно видно, что это два разных объекта разговора: коллекция vs элемент.
        System.out.println(forms); // [/tasks, /tasks/{taskId}]
    }
}

Даже этот простой пример полезен: /tasks — это не “путь к задаче”, а путь к коллекции задач. А /tasks/{taskId} — это уже путь к конкретной задаче. Домен один, но вопросы клиента разные.

3. Представление: как ресурс выглядит наружу

Пожалуй, самое недооценённое слово в REST‑словаре — “представление”. Новичок часто думает так: “Ну ресурс — это JSON, который я отдаю”. И пока проект маленький, кажется, что это работает. Но как только у ресурса появляется хоть какая‑то жизнь (разные поля, разные сценарии, разные формы ответа), выясняется: ресурс — это одно, а его JSON‑форма — другое.

Представление ресурса (representation) — это форма данных, в которой ресурс показывается клиенту или принимается от клиента. Это может быть JSON (в нашем курсе это основной формат), но в общем случае представление может быть и другим: например, у вложения может быть JSON‑метадата и бинарное содержимое файла. Мы детально займёмся файлами позже, но мысль полезно зафиксировать уже сейчас: representation — это то, “как выглядит ресурс в обмене”.

Что важно для проекта Task Tracker API: одна и та же задача может иметь разные представления. Для списка задач обычно удобнее показывать “короткую карточку” (id, title, status), а для подробного просмотра — “детальную карточку” (плюс описание, теги, сроки и так далее). Это не “хитрый трюк”, а обычная потребность клиента.

Мини‑пример (пока не называем это DTO и не уходим глубоко в контрактный дизайн — просто фиксируем идею разных представлений):

import java.time.LocalDate;

// "Ресурсный смысл": задача как сущность домена (внутренняя/богатая форма).
record Task(String id, String title, String description, LocalDate dueDate) {}

// "Представление": упрощённая форма для конкретного сценария (например, список задач).
record TaskSummaryView(String id, String title) {}

public class RepresentationExample {
    static TaskSummaryView toSummary(Task task) {
        // Маппинг в представление: сознательно показываем наружу только нужные поля.
        return new TaskSummaryView(task.id(), task.title());
    }
}

Task здесь — условный “ресурсный смысл” (задача), а TaskSummaryView — одно из представлений. На уровне REST‑мышления мы учимся различать “что существует” и “как мы это показываем”.

4. Ресурс и представление: пример Task Tracker

Путаница “ресурс = JSON” обычно всплывает в самый неудобный момент: когда вы захотели добавить внутреннее поле, поменять формат ответа или скрыть что‑то от клиента. Внутри приложения вы можете хранить больше данных, чем показываете наружу, и это нормально. Наружу должен выходить контролируемый контракт, а ресурс как смысловая сущность при этом остаётся тем же.

Давайте закрепим это различие на одном домене. Возьмём задачу как ресурс и подумаем, как она может выглядеть снаружи.

Понятие Что это по смыслу Пример в Task Tracker Что важно помнить
Ресурс Значимая сущность предметной области “Задача” Это не обязательно Java‑класс и не обязательно таблица
Представление Форма данных “в обмене” “Короткая карточка задачи” для списка, “детальная карточка” для просмотра Представлений может быть несколько
Идентичность Что делает ресурс “тем самым” taskId (UUID string) Идентификатор стабилизирует разговор клиента с API

Ещё один небольшой пример кода, чтобы почувствовать, как внутренняя модель может быть богаче внешнего вида. Мы специально добавим “служебное” поле и не включим его в представление:

import java.time.Instant;

// Внутренняя модель: содержит служебные данные, которые не обязаны быть публичными.
record TaskInternal(String id, String title, String internalNote, Instant createdAt) {}

// Публичное представление: контролируемый контракт для клиента.
record TaskPublicView(String id, String title, Instant createdAt) {}

public class InternalVsPublicExample {
    static TaskPublicView toPublic(TaskInternal task) {
        // Важно: internalNote намеренно не выносится наружу — это часть "контрактной гигиены".
        return new TaskPublicView(task.id(), task.title(), task.createdAt());
    }
}

В реальном проекте internalNote может быть чем угодно: технической меткой, внутренним флагом миграции, деталями хранения в файловом хранилище и так далее. Главное: наличие поля внутри не означает, что оно обязано попасть во внешний контракт. И именно это различие между ресурсом и представлением потом спасает API от хаоса.

5. Подресурс: объект в контексте родителя

Теперь мы подходим к слову, которое чаще всего ломает новичкам мозг: “подресурс”. Подресурс — это тоже ресурс, но такой, который естественно существует в контексте другого ресурса, и чаще всего его жизненный цикл связан с родителем. Это не “кусок JSON внутри JSON”, а отдельная сущность, просто не самостоятельная на верхнем уровне нашей модели.

В Task Tracker API комментарий почти невозможно обсуждать в вакууме. Фраза “удали комментарий c-123” без уточнения задачи звучит подозрительно: а что это за комментарий, к чему он относится? Аналогично и с вложением: вложение “само по себе” пользователю почти не нужно, оно нужно “как вложение к задаче”.

Это и есть основной смысл подресурса: родительский контекст важен для смысла. Поэтому мы читаем comments и attachments как подресурсы задачи.

Пример на уровне модели (опять же — это не про Spring и не про конечный дизайн, а про сам смысл: “есть связь с родительским id”):

import java.time.Instant;

// Подресурс (comment) имеет свою идентичность (id),
// но смысл/жизненный цикл привязан к родительскому ресурсу (taskId).
record Comment(String id, String taskId, String authorName, String text, Instant createdAt) {}

public class SubresourceExample {
    static boolean belongsToTask(Comment comment, String taskId) {
        // Проверка привязки к родителю — в API это будет естественно выражено через URI,
        // а в модели часто выражается через поле taskId.
        return comment.taskId().equals(taskId);
    }
}

Наличие taskId в комментарии подчёркивает идею: комментарий имеет свою идентичность (id), но живёт “под” задачей (taskId). Это модель, которая потом очень естественно ложится на ресурсную карту API.

6. Подресурсы и lookup: comments, attachments, tags

Когда вы начинаете проектировать API, очень легко сделать всё “одинаково важным” и превратить домен в бесконечный CRUD-зоопарк. У новичка это обычно выглядит так: “Раз есть комментарии — значит, нужен полный верхнеуровневый CRUD для comments. Раз есть вложения — значит, ещё один. Раз есть теги — значит, и их туда же”. И вот через два дня у вас уже не Task Tracker, а коллекция разрозненных контроллеров.

В Task Tracker API полезнее сразу разложить роли. Task — основной ресурс. Comment и Attachment — вспомогательные подресурсы задачи: у них есть свои id, но смысл держится на родительской задаче. Клиенту обычно нужен не “комментарий вообще”, а комментарий конкретной задачи; не “файл мира”, а вложение конкретной задачи.

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

Небольшой пример, чтобы “теги как значения” почувствовались руками:

import java.util.Set;
import java.util.TreeSet;

public class TagsExample {
    public static void main(String[] args) {
        // В качестве "значений" теги удобно нормализовать/уникализировать.
        // Здесь для демонстрации используем набор с регистронезависимым сравнением.
        Set<String> tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

        tags.add("backend");
        tags.add("Backend"); // Дубликат с другой капитализацией.

        // Идея: если теги — значения, то их уникальность/нормализация важнее CRUD-жизненного цикла.
        System.out.println(tags); // [backend]
    }
}

Этого различия уже достаточно, чтобы не путать три роли: основной ресурс, подресурс и lookup-значения.

7. Схема‑опора ресурсной модели

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

  • task — основной ресурс, с которым клиент работает как с отдельной адресуемой сущностью;
  • comments и attachments — подресурсы task, потому что их смысл держится на родителе;
  • tags — lookup-коллекция значений, а не параллельная CRUD-вселенная;
  • представления живут рядом с ресурсами и не обязаны совпадать с внутренней моделью.

Этой схемы уже достаточно, чтобы перейти к следующему инженерному вопросу: как на коллекцию и один ресурс ложатся GET, POST, PUT, PATCH, DELETE? Иначе говоря, словарь у нас уже есть — теперь нужна грамматика операций.

8. Типичные ошибки в терминах

Ошибка №1: называть ресурсом любой JSON‑фрагмент.
Часто новичок видит поле status или priority в задаче и говорит: “о, это ресурс”. В итоге появляются странные идеи вроде “давайте сделаем /statuses и /priorities и вообще всё вынесем в отдельные сущности”. Обычно это не улучшает API, а только раздувает его. Ресурс — это не “любая часть данных”, а значимая единица домена, с которой клиент реально взаимодействует как с “вещью”.

Ошибка №2: путать коллекцию и отдельный ресурс, а потом удивляться странным операциям.
Если вы мыслите “список задач” как “одну задачу, только много”, в API появляются гибриды: где-то вы пытаетесь “удалить список”, где-то — “обновить все задачи одним запросом”, где-то — “получить задачу, но без id”. Правильное мышление проще: коллекция отвечает на вопрос “какие есть?”, а отдельный ресурс — “что за конкретная штука?”.

Ошибка №3: считать, что ресурс и представление обязаны совпадать один к одному.
Это приводит к монстру TaskDtoForEverything, который одновременно и для списка, и для деталей, и для создания, и для обновления, и ещё с внезапными полями, которые клиент “не должен трогать, но мы ему отдали, надеюсь, он не тронет”. В нормальном API ресурс стабилен по смыслу, а представления подбираются под сценарии клиента. Сегодня мы это фиксируем терминологически, а позже превратим в практику DTO и контрактов.

Ошибка №4: делать подресурсом всё подряд.
Иногда кажется, что раз у Task есть assigneeName, то “пользователь” — подресурс, значит, /tasks/{id}/assignee. Потом появляются /tasks/{id}/priority, /tasks/{id}/status, /tasks/{id}/title, и API превращается в набор “ручек” к отдельным полям. Подресурс — это отдельная сущность со смыслом и своим жизненным циклом, а не “любое поле в JSON”.

Ошибка №5: раздувать второстепенные элементы до масштаба главного ресурса.
В нашем проекте теги полезны, но они не являются “главной сущностью уровня Task”. Если превратить Tag в полный CRUD‑модуль, мы получим большой объём кода и обсуждений, который не даёт новых знаний про REST‑контракт, а просто увеличивает площадь проекта. В учебном домене нужно уметь вовремя остановиться — это навык не хуже, чем написать ещё один контроллер.

1
Задача
Spring REST & MVC, 3 уровень, 1 лекция
Недоступна
Ресурс и его представление
Ресурс и его представление
1
Задача
Spring REST & MVC, 3 уровень, 1 лекция
Недоступна
Коллекция, ресурс, подресурс и lookup
Коллекция, ресурс, подресурс и lookup
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ