1. Endpoint-map до кода
Карту эндпоинтов лучше собрать до кода. Если оставить это “на потом”, очень быстро выяснится, что у вас есть /tasks/list и /getTasks, которые делают почти одно и то же. Это как ремонт с фразой “да мы по месту решим”: обычно выходит дороже.
Endpoint-map — это “карта метро” вашего API. Она фиксирует, какие адреса существуют, какие методы к ним применимы и какая зона ответственности у каждого пути. Нам важно сделать это до кода по простой причине: код можно переписать, контроллеры можно переименовать, а вот публичные адреса и сама форма API — это то, что клиенты запоминают как контракт.
Есть и вторая практическая причина: карта эндпоинтов помогает согласовать команду (даже если команда — это вы и ваш будущий вы через две недели). Сегодня вы проектируете /api/v1/tasks/{taskId}/attachments/{attachmentId}/download, а завтра уже не будете спорить с самим собой, почему вдруг захотелось /downloadAttachment. Карта — это “точка истины”, чтобы не плодить адреса по памяти.
И наконец, карта эндпоинтов позволяет сразу выделить исключённые антипримеры. Не просто “не делайте так”, а “в каноническую карту это не входит”. Это и есть дисциплина. А дисциплина — это когда вы делаете правильно даже в пятницу вечером, а не только в понедельник утром.
2. Формат эндпоинтов
Чтобы карта не превратилась в клуб споров об именах, сначала фиксируем формат записи. Когда люди не договорились о формате, они начинают спорить о пробелах, тире и о том, “а давайте getAllTasks назовём fetchTasks — так моднее”. Чтобы не превратить проект в клуб любителей именования, мы выбираем простой формат: HTTP_METHOD /path и рядом короткий смысл “что это такое”.
Заметьте: мы сейчас фиксируем только карту адресов и методов. Мы не обсуждаем DTO, валидацию, статус-коды и формат ответов — это отдельные слои контракта. Здесь нам важно, чтобы любой человек (и особенно вы сами) мог открыть карту и мгновенно понять: “Какие входы есть у системы и как они называются?”.
Этого уже достаточно, чтобы карта одинаково читалась человеком, .http-сценарием и будущим маппингом контроллера. Отдельные вспомогательные классы и demo-объекты здесь ничего не добавляют: главный артефакт — сама согласованная таблица адресов.
Прежде чем смотреть на полную карту, полезно ещё раз назвать четыре решения, из которых она собирается:
- на верхнем уровне карты — tasks и lookup-ресурс tags;
- comments и attachments живут как подресурсы задачи;
- изменения состояния не уезжают в командные URI, а /download остаётся узким исключением для бинарного содержимого;
- все адреса начинаются с /api/v1.
Ниже — каноническая карта эндпоинтов проекта. Это наш рабочий ориентир: когда дело дойдёт до @RequestMapping и методов контроллера, адреса уже не придётся придумывать заново.
3. Финальная endpoint-map Task Tracker API
Теперь осталось собрать всё в одну карту. Представьте, что вы рисуете план эвакуации из здания: важно не только “где выход”, но и чтобы все выходы были подписаны одинаково, а лишние двери “в кладовку” не были случайно помечены как “пожарный выход”.
Карту удобнее читать по ресурсным зонам. У нас есть основной ресурс tasks, у него — вспомогательные подресурсы comments и attachments, а также отдельный lookup-ресурс tags. Все пути начинаются с одного базового префикса /api/v1, а идентификаторы ресурсов мы обозначаем через {taskId}, {commentId}, {attachmentId} — это плейсхолдеры, а не реальные значения.
Для наглядности — схема отношений (да, это “картинка из слов”, но очень помогает мозгу):
flowchart TD
API["/api/v1"] --> TASKS["/tasks"]
TASKS --> TASK_ID["/{taskId}"]
TASK_ID --> COMMENTS["/comments"]
COMMENTS --> COMMENT_ID["/{commentId}"]
TASK_ID --> ATT["/attachments"]
ATT --> ATT_ID["/{attachmentId}"]
ATT_ID --> DL["/download"]
API --> TAGS["/tags"]
Ниже — полный список endpoint’ов в канонической версии проекта.
tasks: коллекция задач и отдельная задача
С задач всё начинается, поэтому здесь особенно важно, чтобы путь был предсказуемым. Коллекция задач — это “место, где живут задачи”, а путь с {taskId} — адрес конкретной задачи. CRUD-операции выражаются методами HTTP, а не дополнительными глаголами в URL. Так клиенту проще: он видит один ресурс и разные операции над ним.
| HTTP | Path | Смысл (человеческим языком) |
|---|---|---|
| GET | /api/v1/tasks | Получить список задач |
| POST | /api/v1/tasks | Создать новую задачу |
| GET | /api/v1/tasks/{taskId} | Получить одну задачу по id |
| PUT | /api/v1/tasks/{taskId} | Полностью заменить изменяемое состояние задачи |
| PATCH | /api/v1/tasks/{taskId} | Частично обновить задачу |
| DELETE | /api/v1/tasks/{taskId} | Удалить задачу |
Обратите внимание на дисциплину: везде используется tasks во множественном числе, нет task, Task, myTasks и прочих творческих порывов. Это скучно, зато стабильно. А стабильность в API — очень недооценённая радость (примерно как стабильный интернет).
comments: вспомогательный подресурс внутри задачи
Комментарии в нашем проекте — не “отдельная большая вселенная”, а вспомогательный подресурс, который имеет смысл только в контексте задачи. Поэтому их адрес содержит {taskId}. Такой путь не только красиво выглядит, но и помогает сразу понять границу: “комментарии принадлежат задаче, и без задачи мы их не рассматриваем”.
| HTTP | Path | Смысл (человеческим языком) |
|---|---|---|
| GET | /api/v1/tasks/{taskId}/comments | Получить комментарии задачи |
| POST | /api/v1/tasks/{taskId}/comments | Добавить комментарий к задаче |
| DELETE | /api/v1/tasks/{taskId}/comments/{commentId} | Удалить комментарий задачи |
Заметьте, что у нас нет PUT /comments/{commentId} и прочих операций “редактировать комментарий”. Это не потому, что “так нельзя”, а потому, что мы сознательно держим проект в адекватном объёме: вспомогательный подресурс — это поддержка, а не второй главный ресурс, который перетягивает всё внимание.
attachments: metadata, content и исключение /download
С ресурсом attachments важно удержать уже выбранную границу: это подресурс задачи, а .../download нужен не как бизнес-команда, а как отдельный способ получить бинарное содержимое конкретного вложения. Один путь отвечает за metadata, другой — за content.
| HTTP | Path | Смысл (человеческим языком) |
|---|---|---|
| GET | /api/v1/tasks/{taskId}/attachments | Список метаданных вложений задачи |
| POST | /api/v1/tasks/{taskId}/attachments | Загрузить вложение к задаче |
| GET | /api/v1/tasks/{taskId}/attachments/{attachmentId} | Получить метаданные одного вложения |
| GET | /api/v1/tasks/{taskId}/attachments/{attachmentId}/download | Скачать бинарное содержимое вложения |
| DELETE | /api/v1/tasks/{taskId}/attachments/{attachmentId} | Удалить вложение (метаданные + содержимое) |
Если вас раздражает слово “метаданные”, это нормально. Но смысл тут очень практичный: описание файла, его имя, размер, тип — это одно, а бинарное содержимое — другое. Мы адресуем их разными эндпоинтами, потому что клиенту далеко не всегда нужен сам файл в тот же момент, когда он читает список вложений.
tags: lookup endpoint без отдельного CRUD
Теги — коварный ресурс. Рука так и тянется сделать /tags, /tags/{tagId}, /tags/{tagId}/tasks и так далее. Но в нашем проекте теги — это скорее значение внутри задачи, чем отдельная сущность со своим большим жизненным циклом, а отдельный эндпоинт по тегам нужен как lookup: “покажи уникальные теги, которые есть в системе”. Это удобно клиенту, но не раздувает домен.
| HTTP | Path | Смысл (человеческим языком) |
|---|---|---|
| GET | /api/v1/tags | Получить список уникальных тегов |
Это маленький эндпоинт, но он важен дисциплиной: показывает, что не всё обязано быть “полным CRUD”. Иногда достаточно честного вспомогательного endpoint’а, который решает конкретную прикладную задачу.
4. Исключённые пути
Каноническая карта полезна ещё и тем, что сразу фиксирует антипримеры. Составить “включённые пути” — это только половина дела. Вторая половина — честно и явно записать, что мы не делаем. Это помогает мозгу не “соскальзывать” в привычные плохие решения, особенно когда появляется бизнес-действие (“надо завершить задачу!”) и очень хочется написать /completeTask.
Антипримеры полезны ещё и тем, что показывают стиль, которого мы избегаем. Здесь мы не просто говорим “плохо”, а сразу показываем “как лучше”, чтобы у вас в голове оставался не запрет, а альтернатива.
| Плохой вариант | Почему это плохо | Канонический вариант в нашем API |
|---|---|---|
| GET /api/v1/getTasks | Глагол в пути, метод уже GET | GET /api/v1/tasks |
| GET /api/v1/task | Коллекция в единственном числе, непонятно “одна или список” | GET /api/v1/tasks |
| POST /api/v1/completeTask | RPC-команда вместо работы с ресурсом | PATCH /api/v1/tasks/{taskId} |
| POST /api/v1/changeStatus | Командный endpoint без ресурса в адресе | PATCH /api/v1/tasks/{taskId} |
| POST /api/v1/uploadFileToTask | Команда “загрузить” вместо подресурса | POST /api/v1/tasks/{taskId}/attachments |
| GET /tasks | Выпадает из единого базового пути | GET /api/v1/tasks |
| GET /api/v1/tasks/{taskId}/tags | Сомнительная вложенность: теги не живут “под задачей” как отдельный ресурс | GET /api/v1/tags |
Важно уловить общий принцип: мы не запрещаем “действия”, мы запрещаем превращать путь в команду. Путь — это адрес, а действие — это метод и, в некоторых случаях, способ получить нужное представление ресурса.
5. Типичные ошибки при составлении endpoint-map
Проблема карты эндпоинтов не в том, чтобы её придумать, а в том, чтобы удержать её чистой, когда начинается серия “а давайте ещё вот это быстренько добавим”. Ниже — несколько типичных ошибок, которые почти всегда встречаются у новичков. Это нормально: вы учитесь, а не сдаёте экзамен по телепатии.
Ошибка №1: смешивать “включённые” и “экспериментальные” пути в одной карте.
Часто появляется желание дописать “временно для себя” второй путь вроде /api/v1/getTasks, чтобы “быстрее проверить”. Проблема в том, что временное в API живёт дольше, чем ваша мотивация. Через неделю вы забудете, что это временно, и получите два публичных входа с одинаковым смыслом. Это ломает контракт и создаёт хаос.
Ошибка №2: делать tags вложенным ресурсом, потому что “они же у задачи”.
Да, теги принадлежат задаче как список строк, но это не означает, что для них нужен endpoint /tasks/{taskId}/tags. В нашем проекте теги — value-like часть задачи, а отдельный endpoint нужен как lookup по всей системе. Новички часто путают “поле в ресурсе” и “ресурс в API”, из-за чего получаются лишние и странные пути.
Ошибка №3: пытаться выразить смысл через URI, забывая про HTTP-метод.
Иногда люди пишут /api/v1/tasks/create, /api/v1/tasks/update, /api/v1/tasks/delete, потому что “так понятнее”. Но это ровно тот случай, когда вы делаете протокол в протоколе: у вас уже есть HTTP, и он уже умеет обозначать операции методами. Если вы начинаете дублировать смысл методом и путём, карта становится длиннее и менее предсказуемой.
Ошибка №4: слишком глубокая вложенность “потому что связано”.
Связь сущностей в домене ещё не означает, что путь должен превращаться в роман в трёх томах. Если вы видите что-то вроде /tasks/{taskId}/comments/{commentId}/attachments/{attachmentId}, это обычно сигнал, что границы выбраны неудачно. В нашем проекте вложение привязано к задаче, поэтому оно живёт в /tasks/{taskId}/attachments/..., и этого достаточно.
Ошибка №5: забыть про единый базовый путь и начать “иногда так, иногда так”.
Очень легко по привычке написать один endpoint как /tasks, второй как /api/v1/tasks, третий как /api/tasks. На уровне маленького проекта это выглядит как “ну и что”. На уровне контракта это означает, что API не имеет единой карты и клиенты должны угадывать, где какой стиль. Ровный префикс /api/v1 — это простая дисциплина, которая сильно снижает шанс случайного раздвоения реальности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ