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, а все нюансы (фильтры, выборки, принадлежность) выражаются другими частями контракта, не ломая базовую модель ресурса.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ