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 ресурсу Шлях до одного ресурсу за ідентифікатором /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, а всі нюанси (фільтри, вибірки, належність) виражаються іншими частинами контракту, не ламаючи базову модель ресурсу.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ