JavaRush /Курсы /Spring REST & MVC /URI как часть контракта

URI как часть контракта

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

1. Путь как часть контракта

Путь в API — это часть публичного контракта, а не мелкая техническая деталь. Если раньше вы писали контроллеры “для себя”, легко привыкнуть считать его чем-то, что можно поменять в любую секунду. В реальном API путь — это публичный адрес, как адрес доставки. Вы можете переставить мебель в квартире (рефакторинг кода), но если внезапно поменяете адрес дома (URI), курьер (клиент) приедет в поле, будет грустить и звонить вам в поддержку.

URI (путь) — это часть контракта, которую клиент запоминает. Даже если клиент — это снова вы, только в роли фронтендера “будущего вас”, который через месяц откроет код и скажет: “а почему тут /api/v1/getTasks, а тут /api/v1/tasks/list… кто это сделал?”. У API нет мимики и интонации: оно не может “объяснить” клиенту, что вы имели в виду. Клиент читает контракт буквально: увидел URI — сформировал запрос — ожидает предсказуемое поведение.

И здесь важная мысль для всего курса: хороший URI переживает изменения внутренней реализации. Вы можете переименовать классы, разнести сервисы по пакетам, заменить in-memory репозиторий другим слоем хранения, но /api/v1/tasks должен остаться /api/v1/tasks, если смысл ресурса не менялся. Поэтому URI проектируют “снаружи внутрь”: с позиции потребителя API, а не по принципу “как у нас называется метод в контроллере”.

2. Анатомия URI: сегменты и шаблоны

Прежде чем спорить, “tasks” у нас или “task-items”, полезно договориться о базовых терминах. URI в HTTP обычно состоит из пути и, возможно, строки запроса, например /api/v1/tasks?status=TODO. Сегодня мы фокусируемся именно на путях: это та часть адреса, которая разбита на сегменты через / и показывает, к какому ресурсу мы обращаемся. Параметры запроса пока оставим в стороне: для разговора о пути нам важно адресное дерево ресурса, а не настройка запроса вроде status=TODO.

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

Небольшая шпаргалка по терминам (без занудства, но с пользой):

Термин Что это Пример
URI “Адрес” ресурса (в контексте HTTP API чаще говорим про path + query) /api/v1/tasks/123
Сегмент пути Часть URI между / api, v1, tasks, 123
URI коллекции Путь к набору однотипных ресурсов /api/v1/tasks
URI ресурса Путь к одному ресурсу по id /api/v1/tasks/{taskId}
URI-шаблон Запись пути с плейсхолдером (удобно в документации и в коде) /api/v1/tasks/{taskId}

Чтобы визуально разложить путь на сегменты, иногда помогает совсем простая схема. Да, схема в лекции про URI — это уже лёгкая инженерная драма, но лучше так, чем потом искать баги в несуществующем эндпоинте:

flowchart LR
    A["/api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f"]
    A --> B["api"]
    B --> C["v1"]
    C --> D["tasks"]
    D --> E["7b9b... (taskId)"]

Обратите внимание на важный нюанс: в реальном запросе вы никогда не отправляете фигурные скобки. {taskId} — это лишь удобный “макет” для документации и описания маршрутов в коде. В реальном запросе там будет конкретное значение, например UUID.

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

Большая часть API строится из двух кирпичиков: “коллекция” и “один ресурс”. Когда начинающий разработчик слышит “спроектировать API”, он часто рисует в голове десятки путей. На практике, если аккуратно выбрать URI коллекции, URI отдельного ресурса получается почти автоматически: достаточно добавить идентификатор. И это отличная новость — согласованность тут можно получить почти бесплатно.

В Task Tracker API базовый пример — задачи. Коллекция задач — это список, с которым клиент работает как с набором элементов. Один ресурс задачи — это конкретная задача по id. Уже на этом уровне можно сделать API предсказуемым: клиент, увидев /tasks, почти без подсказок ожидает, что /tasks/{taskId} даст одну конкретную задачу.

Для закрепления — маленькая табличка. Мы используем полный префикс /api/v1, но сейчас важна не логика версии, а сама логика адреса: путь начинается с имени ресурса.

Что адресуем URI Как это читается человеком
Коллекция задач /api/v1/tasks “Все задачи” (точнее: набор задач)
Одна задача /api/v1/tasks/{taskId} “Задача с идентификатором taskId”

Этого различения пока достаточно: коллекция живёт по /api/v1/tasks, а конкретный ресурс — по /api/v1/tasks/{taskId}. На уровне кода такие строки тоже лучше держать централизованно, но сама формула простая: сначала имя коллекции, потом идентификатор ресурса.

4. Имена коллекций: множественное число

Коллекции лучше называть предсказуемо, а не оригинально. На этом шаге легко уйти в креатив: назвать задачи как-нибудь необычно, например /issues, /todoItems, /mySuperTasks. И креатив — это прекрасно… ровно до момента, когда вы вспоминаете, что у API есть потребитель. Потребителю не нужно ваше вдохновение — ему нужна предсказуемость. Поэтому в API-дизайне мы обычно выбираем скучное, но стабильное: понятные существительные и единый стиль именования.

Практическое правило для коллекций в REST API: коллекции называем во множественном числе. Это не закон физики, а конвенция, которая снижает когнитивную нагрузку. /tasks воспринимается как список. /task воспринимается как “одна задача”, а потом начинается путаница: “почему /task возвращает массив?”. Никакой трагедии не случится, но API станет менее читаемым — а это как маленькая трещина в фундаменте: сегодня её не видно, а через месяц вы будете жить в доме из трещин.

В нашем проекте словарь ресурсов уже намечен доменом: tasks, comments, attachments, tags. Здесь важно не только “как назвать”, но и держать одно имя везде. Если вы сегодня называете ресурс tasks, не надо завтра заводить рядом /todo “потому что так короче”. Коротко будет первые 10 минут, а потом вы получите два параллельных мира, которые делают одно и то же.

Хорошее и плохое иногда проще показать прямо лоб в лоб:

Смысл Плохо Хорошо Почему
Коллекция задач /api/v1/task /api/v1/tasks Множественное число сразу намекает на список
“Получить задачи” /api/v1/getTasks /api/v1/tasks Действие выражает HTTP-метод, не путь
“Список задач” /api/v1/taskList /api/v1/tasks Не нужно дублировать “list” — коллекция и так список

Отдельный нюанс, который часто забывают: URI — не место для внутренних терминов вашей кодовой базы. Клиенту всё равно, что у вас в Java-коде класс называется TaskAggregateRoot или TaskEntity. В URI мы говорим на языке предметной области, но в “человеческом” виде. Если предметная область — задачи, то ресурс называется tasks, а не task-aggregates.

5. Идентификатор в пути: {taskId}

Идентификатор в пути задаёт адрес конкретного ресурса. Момент с id кажется простым: ну id и id, что тут обсуждать. Но именно здесь многие API начинают путаться. URI ресурса — это адрес конкретной сущности, а значит идентификатор должен жить в пути. Если вы адресовали одну задачу, логично идти по адресу /tasks/{taskId}. Это похоже на квартиру в доме: дом — это “коллекция квартир”, а номер квартиры — конкретный ресурс внутри.

В Task Tracker API идентификаторы ресурсов у нас заданы как UUID-строки. Это и для учебных целей удобно, и достаточно реалистично: UUID не раскрывает бизнес-смысл, его нельзя “угадать перебором”, и он остаётся стабильным даже если потом поменяется способ хранения данных. На уровне URI важно помнить: идентификатор должен быть непрозрачным для клиента и стабильным. Клиенту не нужно знать, что “42” — это “сорок вторая запись в базе”.

Ещё один полезный договор: в шаблонах пути мы используем говорящие имена плейсхолдеров. Да, можно написать {id}, но как только появится вложенность (а у нас в проекте будут taskId, commentId, attachmentId), вы сами себе скажете спасибо за явные имена. Они немного длиннее, зато исчезает вопрос “а это id чего именно?”.

Сравните “шаблон” и “реальный URI”:

- Шаблон в документации или в описании маршрута: /api/v1/tasks/{taskId}
- Реальный запрос: /api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f

Здесь важно буквально увидеть форму адреса: /api/v1/tasks/{taskId} — это шаблон, а /api/v1/tasks/7b9b... — реальный URI. Нас сейчас интересует не тип taskId, а то, что адрес одного ресурса получается из адреса коллекции плюс идентификатор.

6. Глаголы в пути и HTTP-методы

Глаголы в пути почти всегда лишние: в HTTP для действия уже есть метод запроса. Если у вас когда-то был опыт “API как набор функций”, рука сама тянется к путям вроде /getTasks, /createTask, /deleteTask. Это выглядит логично, пока не вспомнить: в HTTP уже есть глагол — это метод запроса. Когда вы пишете глагол ещё и в пути, получается двойное кодирование смысла. А двойное кодирование — лучший друг противоречий: сегодня у вас GET /createTask, завтра POST /getTasks, и вот вы уже живёте в мире, где URI и метод конфликтуют между собой.

В REST-подходе путь отвечает на вопрос “какой ресурс?”, а HTTP-метод — на вопрос “что сделать?”. Если нам нужны задачи, мы идём по /tasks. Если мы хотим создать задачу — мы всё ещё работаем с ресурсом задач, просто метод будет другой. Это одна из причин, почему REST API читается как система, а не как “каталог команд”.

Очень короткая иллюстрация, почти мемная:

// Пример: одно и то же действие ("получить список") не должно кодироваться глаголом в URI.
String bad = "/api/v1/getTasks";
String good = "/api/v1/tasks";

// Смысл операции выражается HTTP-методом, а не глаголом в пути.

И если записать это как строки запросов (только как иллюстрацию контракта, без деталей ответов), разница становится ещё понятнее:

  • Плохо: GET /api/v1/getTasks
  • Хорошо: GET /api/v1/tasks

Плохой вариант рассказывает “что сделать”, но не показывает ресурсную модель. Хороший вариант показывает ресурсную модель, а действие уже читается из HTTP-метода. Это и есть проверка на зрелость: если вы видите /api/v1/getTasks, то у вас почти наверняка мышление в стиле RPC. Ничего страшного — оно у всех сначала бывает, просто сегодня мы его сознательно лечим.

7. Стабильность URI и единый стиль

Недостаточно сделать путь просто рабочим — важно сделать его устойчивым. Путь легко сделать “работающим”, но куда труднее сделать его таким, чтобы через полгода он выглядел так же логично, как в день написания. Стабильность URI — это не про “никогда ничего не менять”, а про то, чтобы изменения были редкими, осознанными и оправданными. Если вы меняете URI потому, что “переименовал метод в контроллере”, — это почти всегда неправильная причина.

Единообразие начинается с мелких решений. Например: пишем сегменты в нижнем регистре (tasks, а не Tasks), стараемся держать сегменты короткими, не используем подчёркивания (task_items) без крайней необходимости, не добавляем расширения файлов (tasks.json), не плодим синонимы. API — это как словарь: если один и тот же смысл называется разными словами, читатель (клиент) не понимает, это “то же самое” или “что-то другое”.

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

Выбор Рекомендация для курса Почему это помогает
Регистр lowercase Визуально едино, меньше неожиданностей
Имена ресурсов существительные Путь описывает “что”, не “как”
Коллекции множественное число Быстро читается как список
Слэш в конце без завершающего слэша Меньше дубликатов вида /tasks и /tasks/
Синонимы запрещаем внутри одного API Не превращаем карту API в квест

И есть ещё одна вещь, о которой легко забыть: URI должен быть клиентским. Внутри сервиса вы можете перекраивать архитектуру как угодно (сегодня сервисы, завтра отдельные сценарии, послезавтра “я переписал всё на функциональный стиль и теперь счастлив”). Клиенту всё равно. Ему важно, что /api/v1/tasks всегда означает одно и то же, независимо от вашей внутренней философии.

8. Словарь имён ресурсов

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

В Task Tracker API у нас есть четыре главных имени ресурсов, которые вы будете видеть снова и снова: tasks, comments, attachments, tags. Эти имена выбраны не потому, что “так принято в интернете”, а потому, что они напрямую отражают домен проекта и читаются однозначно. Если вы видите attachments, вы не думаете “а это точно файлы или это список ссылок на документы?”, вы понимаете: это вложения, то есть что-то прикреплённое к задаче.

Можно зафиксировать это в маленькой “таблице словаря”:

Доменный объект Имя ресурса (сегмент) Комментарий
Task tasks Главный ресурс проекта
Comment comments Вспомогательный ресурс рядом с задачами
Attachment attachments Вспомогательный ресурс для файлов и метаданных
Tag tags Справочный ресурс (без большого CRUD)

Важно, что мы не придумываем альтернативные названия: не делаем todo, не делаем work-items, не делаем files вместо attachments. Одна сущность — одно имя. Это и есть контрактная дисциплина: клиент должен “выучить” словарь один раз и дальше ориентироваться по карте API без сюрпризов.

Пути в коде: один источник истины

На уровне кода API чаще всего “плывёт” из-за банального копирования строк. Вы написали "/api/v1/tasks" в одном файле, потом где-то руками набрали "/api/v1/task" (без s) — и вот у вас уже два разных мира. IDE и компилятор промолчат, а клиент пожалуется поздно и больно. Поэтому полезно держать базовый префикс, имена ресурсов и шаблоны вроде /tasks/{taskId} в одном месте.

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

9. Типичные ошибки при проектировании URI

Ошибки в URI редко ломают код сразу, но почти всегда дорого обходятся позже. Код компилируется, приложение запускается, а проблема всплывает потом — когда клиент не может найти эндпоинт, документация начинает противоречить реализации, а команда тратит время на “почему 404?”. Поэтому полезно заранее проговорить типичные грабли и научиться распознавать их как запах дыма, а не отмахиваться: “ну, мелочь”.

Ошибка №1: коллекция в единственном числе.
Путь вида /api/v1/task для списка задач почти всегда рождает путаницу: “это одна задача или список?”. Множественное число (/api/v1/tasks) делает намерение очевидным и экономит объяснения. Это маленькое правило, но оно резко повышает читаемость карты API.

Ошибка №2: глаголы в пути (/getTasks, /createTask, /deleteTask).
Такое API начинает жить как каталог команд, а не как модель ресурсов. Действие должно жить в HTTP-методе, а не в имени ресурса. Иначе вы получаете двойной смысл, который со временем превращается в противоречия и случайные решения.

Ошибка №3: несколько имён для одного и того же смысла.
Сегодня вы называете ресурс tasks, завтра заводите todo, потом появляется work-items. Для клиента это выглядит так, будто у вас три разных сущности, хотя смысл один. Внутри проекта это тоже разрушает дисциплину: появляются разные DTO, разные контроллеры, разные “почти одинаковые” сценарии. Лучше держать один словарь и не плодить синонимы.

Ошибка №4: подгонка URI под внутренние названия классов.
URI вроде /api/v1/taskController/getAll может показаться “логичным” ровно до первого рефакторинга. Клиент не должен знать, как вы назвали классы и методы. Путь должен выражать домен и быть стабильным при изменениях реализации.

Ошибка №5: слишком “умные” или “оригинальные” пути.
Иногда хочется сделать красиво: /api/v1/myTasks, /api/v1/tasksForMe, /api/v1/allTasks. Но “красиво” часто означает “непредсказуемо”. Для клиента гораздо лучше, когда есть базовый ресурс /tasks, а все нюансы (фильтры, выборки, принадлежность) выражаются другими частями контракта, не ломая базовую модель ресурса.

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