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 | Шлях | Зміст (людською мовою) |
|---|---|---|
| GET | /api/v1/tasks | Отримати список задач |
| POST | /api/v1/tasks | Створити нову задачу |
| GET | /api/v1/tasks/{taskId} | Отримати одну задачу за ідентифікатором |
| PUT | /api/v1/tasks/{taskId} | Повністю замінити стан задачі, що змінюється |
| PATCH | /api/v1/tasks/{taskId} | Частково оновити задачу |
| DELETE | /api/v1/tasks/{taskId} | Видалити задачу |
Зверніть увагу на дисципліну: всюди використовується tasks у множині, немає task, Task, myTasks та інших творчих поривів. Це нудно, зате стабільно. А стабільність в API — дуже недооцінена радість, приблизно як стабільний інтернет.
comments: допоміжний підресурс у межах задачі
Коментарі в нашому проєкті — не «окремий великий всесвіт», а допоміжний підресурс, який має сенс лише в контексті задачі. Тому їхня адреса містить {taskId}. Такий шлях не лише гарно виглядає, а й допомагає одразу зрозуміти межу: коментарі належать задачі, і без задачі ми їх не розглядаємо.
| HTTP | Шлях | Зміст (людською мовою) |
|---|---|---|
| GET | /api/v1/tasks/{taskId}/comments | Отримати коментарі задачі |
| POST | /api/v1/tasks/{taskId}/comments | Додати коментар до задачі |
| DELETE | /api/v1/tasks/{taskId}/comments/{commentId} | Видалити коментар задачі |
Зверніть увагу, що у нас немає PUT /comments/{commentId} та інших операцій «редагувати коментар». Це не тому, що так не можна, а тому, що ми свідомо тримаємо проєкт в адекватному обсязі: допоміжний підресурс — це підтримка, а не другий головний ресурс, який перетягує всю увагу.
attachments: метадані, вміст і виняток /download
З ресурсом attachments важливо втримати вже обрану межу: це підресурс задачі, а .../download потрібен не як бізнес-команда, а як окремий спосіб отримати бінарний вміст конкретного вкладення. Один шлях відповідає за метадані, інший — за вміст.
| HTTP | Шлях | Зміст (людською мовою) |
|---|---|---|
| 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 | Шлях | Зміст (людською мовою) |
|---|---|---|
| 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 — це проста дисципліна, яка сильно знижує шанс випадкового роздвоєння реальності.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ