JavaRush /Курси /Spring REST & MVC /REST: зовнішній контракт API

REST: зовнішній контракт API

Spring REST & MVC
Рівень 3 , Лекція 0
Відкрита

1. Проблема: командний API перетворюється на хаос

Майже кожен початківець бекенд-розробник рано чи пізно проєктує API «як пульт від телевізора»: під кожну дію — окрема кнопка. Спочатку це навіть здається зручним. Є дія «створити задачу» — отже, буде /createTask. Є дія «закрити задачу» — буде /completeTask. Здається логічним: ви буквально переносите свої думки в URL. Але щойно система починає жити, кнопок стає 50, потім 150, і в якийсь момент ви вже самі не памʼятаєте, де у вас «завершити», а де «архівувати», і чим одне відрізняється від іншого.

Уявіть, що в Task Tracker потрібно додати ще трохи функціональності. Користувач хоче призначити виконавця, зняти виконавця, змінити пріоритет, поставити дедлайн, прибрати дедлайн, додати коментар, видалити коментар, прикріпити файл, видалити файл, повторно відкрити задачу… Якщо проєктувати це командно, дуже швидко вийде ціла колекція кінцевих точок-заклинань, де клієнтові потрібно не розбиратися в домені, а вивчити словник ваших дієслів.

Ось тут і проявляється головна проблема: клієнтові не потрібен список ваших внутрішніх «команд». Йому потрібен зрозумілий набір об’єктів і операцій, які передбачувано виглядають і передбачувано поводяться.

HTTP уже задав базову граматику: method, path, headers, body, status. Але однієї граматики замало, якщо сама поверхня API влаштована як каталог команд. Тепер важливо зрозуміти, навколо чого взагалі розкладати домен, щоб GET, POST та інші методи працювали на зрозумілій мапі, а не на наборі кнопок.

REST у цьому курсі: не релігія, а спосіб домовитися з клієнтом

Коли люди чують «REST», часто починається або релігійна війна, або ж легка паніка. «А раптом ми не робимо справжній REST?» «А HATEOAS потрібен?» «А можна дієслова в URL?» Давайте чесно: у комерційній розробці REST — це не іспит із філософії. Це практичний спосіб зробити API читабельним, передбачуваним і обговорюваним з командою та клієнтами.

У межах цього курсу REST — це насамперед зовнішній контракт. Тобто те, що бачить споживач вашого API: мобільний застосунок, фронтенд, інтеграція партнера, Postman, автотести, іноді навіть інший бекенд. REST допомагає перестати думати: «який метод контролера я зараз напишу?» — і почати думати: «який об’єкт нашої предметної області існує для клієнта?»

Ще одну плутанину варто зняти одразу. REST не диктує внутрішню архітектуру. Усередині у вас може бути хоч 30 сервісів, хоч один величезний (так робити не варто, але буває), хоч шари controller -> service -> repository. Клієнту байдуже. REST — це про те, як виглядає поверхня системи зовні, а не про те, як улаштована кухня всередині.

2. Зовнішній контракт vs імена методів

Щоб переосмислити підхід, корисна проста аналогія: API — це меню в ресторані. Клієнт бачить меню. Він не знає, як кухар назвав змінну sauce2 і який метод викликають на кухні. Якщо завтра ви перейменуєте cookCarbonara() на makePastaLikeInRome() — відвідувачу ресторану байдуже. Але якщо ви раптом приберете з меню «Карбонарy» або перейменуєте її на «Макарони_42», відвідувачу буде боляче, і він піде до конкурентів. А якщо це корпоративна інтеграція, то надішле до підтримки дуже ввічливого листа, у якому все одно буде чути крик.

Так само і з API. Усередині коду у вас справді можуть бути команди. У вас може бути сервіс із методами completeTask(taskId) або addComment(taskId, text). Це нормальна внутрішня модель поведінки. Проблема починається, коли ви вирішуєте: «Раз у мене є метод completeTask, значить, і URL буде /completeTask». Тобто зовнішній контракт стає дзеркалом внутрішніх імен методів.

Невелика ілюстрація на Java. Внутрішні команди застосунку можуть виглядати так:

class TaskCommands {
    // Внутрішня модель: «команди» застосунку, тобто те, як ви називаєте методи у своєму коді
    void createTask() {}

    // taskId — внутрішній ідентифікатор задачі. Клієнту важливо, що це саме «задача», а не «команда»
    void completeTask(String taskId) {}
}

А зовнішній контракт — це те, які дані ви показуєте й як до них звертаєтеся. Його зручніше мислити так: «ось задача, ось її представлення для клієнта»:

// Зовнішня модель: DTO/представлення ресурсу «задача» для клієнта
record TaskView(String id, String title, String status) {}

Сенс не в тому, що record магічно перетворює API на REST. Сенс у тому, що зовнішній світ має говорити мовою задач, а не мовою внутрішніх команд.

3. Перехід до предметної області клієнта

Якщо з цієї лекції запам’ятається лише одна фраза, нехай це буде вона: у REST-мисленні головне питання звучить так: «З чим саме працює клієнт?» Не «що ми робимо», не «який метод ми викликаємо», а «який об’єкт предметної області тут головний».

Для Task Tracker API відповідь очевидна: головний об’єкт — задача. Саме навколо неї будується сенс системи. Коментарі належать задачі. Вкладення — теж. Теги в нашому навчальному проєкті — радше зручні значення для пошуку та класифікації задач, а не окрема велика сутність зі своїм життям. Статус — частина стану задачі, а не самостійний «об’єкт команди».

Приємний побічний ефект цього питання: воно змушує думати не «куди додати кінцеву точку», а «яка частина домену зараз обговорюється». А отже, API починає нагадувати мапу місцевості, а не список випадкових заклинань.

Ось короткий приклад, який показує різницю в мисленні, не заглиблюючись у Spring та анотації:

import java.util.List;

// "Командний" стиль: кінцева точка == дієслово/операція, яку треба запам'ятати
List<String> rpcLike = List.of(
    "/createTask",
    "/completeTask",
    "/getAllTasks"
);

// "Ресурсний" стиль: кінцева точка == сутність/ресурс, з яким працює клієнт
List<String> resourceLike = List.of(
    "/tasks",
    "/tasks/{taskId}"
);

Перший набір звучить як набір кнопок на пульті. Другий — як «у нас є задачі; іноді ми працюємо зі списком, іноді — з конкретною задачею». І це «іноді зі списком, іноді — з конкретною» уже перший крок до стійкого контракту.

4. RPC-like стиль: зручно, але погано живе

RPC-like API — це коли зовнішній інтерфейс системи виглядає як «віддалений виклик методів». URL перетворюється на ім’я команди, а HTTP-запит за змістом зводиться до «виклич-но мені ось цю функцію на сервері». З погляду початківця це й справді здається зручним: ви пишете метод і майже тими самими словами називаєте кінцеву точку. Мінімум роздумів, максимум швидкості. Майже як копіпаст, тільки в голові.

Але в такого підходу є неприємна особливість: він погано масштабується. Кожен новий сценарій додає новий дієслівний маршрут. До того ж він змішує дві речі, які HTTP спеціально розводить: адресу (path) і дію (method). У результаті ви легко приходите до чогось на кшталт GET /deleteTask — тобто «читанням» запускаєте «видалення». HTTP від такого сумує, кешування плутається, idempotency починає звучати як лайка, а клієнтові доводиться запам’ятовувати винятки.

Порівняймо на рівні сенсу, а не реалізації. Ось так виглядає «командна» форма:

import java.util.Map;

// Ключ — "що хочемо зробити", значення — "як саме викликати команду через HTTP"
Map<String, String> commands = Map.of(
    "complete", "POST /completeTask",
    "delete", "GET /deleteTask/{taskId}"
);

А ось так починає виглядати «ресурсна» форма, де головний об’єкт — задача:

import java.util.Map;

// Ключ — "який ресурс", значення — "як із ним працює HTTP-метод + шлях"
Map<String, String> resources = Map.of(
    "task collection", "POST /tasks",
    "one task", "DELETE /tasks/{taskId}"
);

Зверніть увагу: зараз узагалі неважливо, який саме HTTP-метод виявиться правильним у кожному сценарії — це ми розбиратимемо пізніше в курсі. Важливіше інше: у другому варіанті одна й та сама предметна сутність читається однаково. І тому API набагато простіше обговорювати. Ви не обговорюєте «а яка в нас кінцева точка для завершення?», ви обговорюєте: «що означає завершити задачу як зміну її стану?».

5. Однакові операції мають виглядати однаково

REST цінний не тим, що робить URL «красивими», а тим, що робить API впізнаваним. Впізнаваність — це коли клієнт, уперше побачивши частину API, з великою ймовірністю може вгадати решту. Не тому, що він телепат, а тому, що ви проєктуєте контракт системно: схоже — значить, і виглядає схоже.

Передбачуваність особливо рятує, коли API зростає. У вас з’являється більше ресурсів, більше сценаріїв, більше розробників у команді. І ось тут страшна правда: ніхто не втримає в голові сотню «унікальних» кінцевих точок. А от набір ресурсів і зрозумілі правила «як із ними зазвичай працюють» втримати реально.

Це можна уявити як мову. Якщо в мові немає граматики, залишається зубріння. «Ця команда називається так, а ця — так, а ось ця — виняток». Якщо граматика є, ви можете будувати нові фрази, не заучуючи кожну окремо. REST — це така «граматика» для API на базі HTTP: адресуємо предметну область через URI, а характер операції виражаємо методами та зрозумілими домовленостями.

Щоб відчути це інженерно, достатньо простої мініперевірки: якщо я бачу /tasks/{taskId}, чи можу я хоча б приблизно очікувати, що операції навколо задачі будуть якось пов’язані з цим самим шляхом, а не розлетяться по /doSomethingWithTask? У ресурсному стилі відповідь зазвичай «так». У командному — «не знаю, треба пошукати в документації… якщо вона взагалі є».

6. Мінікейс на Task Tracker API

Зараз ми не проєктуємо фінальні URI і не затверджуємо версію /api/v1 (це буде пізніше). Але вже зараз можна побачити, як один і той самий домен або стає зрозумілим, або перетворюється на кашу — залежно від обраного стилю.

Уявімо, що ви хочете покрити базові сценарії Task Tracker (створити задачу, подивитися список, подивитися одну, додати коментар, завантажити вкладення, отримати список тегів). Командний стиль дуже легко сповзає ось у це:

import java.util.List;

// Погана масштабованість: нові можливості = нові "дієслівні" кінцеві точки
List<String> rpcStyle = List.of(
    "/createTask",
    "/getAllTasks",
    "/getTaskById",
    "/addCommentToTask",
    "/uploadFileToTask",
    "/getAllTags"
);

На старті здається: «ну окей». Але спробуйте продовжувати цей список ще тиждень розробки. Дуже швидко вийде вибух дієслівних маршрутів: під кожну дрібницю — окрема кінцева точка, часто ще й із дієсловами різних форм (create, add, upload, getAll, find, fetch…), і ніхто вже не гарантує послідовності.

Ресурсний стиль, навпаки, змушує згрупувати все навколо головного об’єкта:

import java.util.List;

// "Мапа" предметної області: є задачі й підресурси задач, плюс окремі довідники
List<String> resourceStyle = List.of(
    "/tasks",
    "/tasks/{taskId}",
    "/tasks/{taskId}/comments",
    "/tasks/{taskId}/attachments",
    "/tags"
);

І ось тут у голові з’являється «мапа»: є задачі, у задачі є коментарі та вкладення, а теги — окремий невеликий список значень. Навіть якщо ви поки не знаєте, які методи і статуси будуть у кожної кінцевої точки, поверхня системи вже стала читабельною.

Зверніть увагу ще на один важливий момент. У другому варіанті ви майже автоматично переходите до розмови про межі. Наприклад, коментарі живуть «під задачею» — отже, їх логічно адресувати як частину задачі. А теги в нашому навчальному проєкті не стають окремим великим CRUD-ресурсом — отже, вони живуть як окремий довідковий endpoint. Ці рішення не про «правильно/неправильно» самі по собі. Вони про те, щоб навчальний проєкт залишався компактним, зрозумілим і розвивався без зайвого ускладнення.

7. Типові помилки під час проєктування REST API

Помилка №1: плутати REST із «красивими path».
Дуже легко звести REST до косметики: «давайте просто зробимо URL у множині — і буде REST». Проблема в тому, що множина сама по собі не перетворює набір випадкових дій на контракт. Щоб не потрапити в цю пастку, корисно щоразу повертатися до питання «який об’єкт предметної області ми показуємо клієнту?». Якщо відповіді немає, значить, ви поки вигадуєте не ресурс, а команду.

Помилка №2: думати, що REST диктує внутрішню архітектуру застосунку.
Іноді починають сперечатися: «якщо REST, значить сервіси мають бути такими-то», «значить, не можна мати метод completeTask()». Можна. Усередині коду у вас можуть бути будь-які команди, методи й навіть дієслова в назвах — це нормально. REST дисциплінує зовнішній контракт. Внутрішню реалізацію ви змінюватимете десятки разів, і саме тому зовнішній шар має бути стабільним і незалежним від ваших внутрішніх перейменувань.

Помилка №3: копіювати у зовнішній API імена внутрішніх методів.
Це найчастіший «перехід за інерцією»: є метод uploadFileToTask() — значить, буде /uploadFileToTask. У результаті зовнішній контракт починає відображати вашу поточну реалізацію, а не домен. Лікується це одним прийомом: уявіть, що клієнт не знає ваш код і ніколи його не побачить. Клієнт розуміє лише домен («задачі, коментарі, вкладення»). Тоді URL із командами одразу перестає виглядати природно.

Помилка №4: сприймати REST як догму або як іспит на «чистоту».
У реальному житті REST — це інструмент ясності, а не змагання в дусі «хто ближчий до RFC». Якщо намагатися ідеально відповідати всім теоретичним обмеженням, можна втонути в деталях і зупинити розробку. З іншого боку, якщо сказати «REST — це коли як завгодно», вийде хаос. Прагматичний підхід у нашому курсі такий: ми тримаємо ресурсну модель читабельною і послідовною, але не перетворюємо проєкт на академічну дискусію.

Помилка №5: втрачати центральний об’єкт системи.
У Task Tracker API центральний об’єкт — задача. Якщо ви проєктуєте API так, що «головного об’єкта» не видно (усе розмазано по командах, а сутність task з’являється лише як параметр десь углибині), ви втрачаєте мапу предметної області. Тоді будь-які нові фічі додаються не як розширення системи, а як нові хаотичні кнопки. Гарна самоперевірка: чи можна за 10 секунд пояснити за списком кінцевих точок, «про що цей сервіс»? Якщо ні, ви, найімовірніше, знову пішли в командний стиль.

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