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 ресурсу | Шлях до одного ресурсу за ідентифікатором | /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 базовий приклад — задачі. Колекція задач — це список, з яким клієнт працює як із набором елементів. Один ресурс задачі — це конкретна задача за ідентифікатором. Уже на цьому рівні можна зробити 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, а всі нюанси (фільтри, вибірки, належність) виражаються іншими частинами контракту, не ламаючи базову модель ресурсу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ