JavaRush /Курси /Go SELF /Дизайн ресурсів: /api/v1/tasks, /api/v1/tasks/{id} і /don...

Дизайн ресурсів: /api/v1/tasks, /api/v1/tasks/{id} і /done

Go SELF
Рівень 56 , Лекція 2
Відкрита

1. Навіщо мислити ресурсами, а не командами

Коли ви починаєте робити HTTP‑API, дуже легко скотитися до стилю «зручних команд»: /createTask, /getTask, /markTaskDone. Це схоже на пульт керування: кнопка «Створити», кнопка «Отримати», кнопка «Зробити красиво». Проблема в тому, що HTTP уже придумав для вас кнопки — це методи. А шлях має описувати яка саме сутність стоїть за цією адресою.

Ресурсний підхід — це коли ви уявляєте API як «вітрину сутностей». Є колекція задач, є конкретна задача, є подання стану «виконано». Тоді клієнту простіше передбачати API за формою, а не вивчати 30 унікальних URL‑команд.

Хороша новина: вам не потрібно знати всю REST‑філософію. Достатньо тримати в голові одну просту фразу:

URL відповідає на запитання «що це?», а метод — «що зробити?»

Мініпорівняння: командний стиль vs ресурсний

Що хочемо Командний URL (погано) Ресурсний URL (нормально)
отримати список задач
GET /getTasks
GET /api/v1/tasks
створити задачу
POST /createTask
POST /api/v1/tasks
отримати задачу 7
GET /getTask?id=7
GET /api/v1/tasks/7
позначити задачу 7 виконаною
POST /markDone?id=7
POST /api/v1/tasks/7/done

Командний стиль виглядає «зрозумілим» рівно доти, доки API не починає розростатися. Потім стартує дрейф: createTask, addTask, newTask, makeTask — і раптом у вас в API з’являється лінгвістичний курс із синонімів англійських дієслів.

2. Версія в шляху: навіщо потрібен /api/v1

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

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

У світі Go дуже добре відчувається важливість сумісності: якщо ви змінюєте публічний API (функції, типи), ви ризикуєте зламати чужий код, який на нього спирається. Це прямо описують як проблему сумісності: «не можна забирати API, інакше програми зламаються». З HTTP‑контрактами та сама історія, просто «чужа програма» — це клієнт вашого API.

Тому /api/v1 — це спосіб чесно сказати: «ось правила версії 1». Якщо колись знадобляться несумісні зміни, у вас є варіант /api/v2, і старі клієнти не мусять страждати.

Невеликий приклад на Go: фіксуємо префікс версії

package main

const apiV1 = "/api/v1"

func tasksPath() string {
	return apiV1 + "/tasks"
}

Тут немає магії — просто звичка: усі шляхи будуємо від єдиного префікса. Це знижує ризик друкарських помилок і допомагає тримати контракт цілісним.

3. Колекція задач: /api/v1/tasks

Шлях /api/v1/tasks — це колекція задач. Уявіть полицю в магазині: на ній стоять усі товари. З полицею зазвичай роблять дві речі: дивляться, що на ній є, і ставлять новий товар.

Так само й в API: список задач — це запит до колекції, а створення задачі — це додавання до неї. Розрізняєте ці дії ви методом, а шлях лишається тим самим.

Тут важлива психологічна річ: коли шлях один, а метод змінюється, API сприймається як форма, а не як зоопарк посилань. Клієнту легше запамʼятати /tasks і далі вже логічно здогадатися: GET — читаю, POST — створюю.

Приклад: описуємо endpoint як об’єкт контракту

package main

type Endpoint struct {
	Method string
	Path   string
}

var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}

Якщо ви зараз подумали: «навіщо мені це в Go‑коді, ми ж іще сервер не пишемо?», то відповідь проста: так ви тренуєте м’яз «спочатку контракт». Цей самий список потім можна буде використати і в документації, і в тестах, і навіть у логах.

Чому tasks у множині

/tasks, а не /task, — тому що колекція передбачає множину. Так, це радше правило гарного тону, але воно реально допомагає мисленню. Якщо бачите множину, ви очікуєте список. Якщо бачите /tasks/7, ви очікуєте один елемент.

4. Одна задача: /api/v1/tasks/{id}

Якщо /tasks — це полиця, то /tasks/{id} — це картка конкретної задачі. Тут {id} — це шаблон у документації, а не реальні фігурні дужки в URL.

Тобто в контракті ви пишете:

  • /api/v1/tasks/{id}

а в реальному запиті клієнт підставляє число:

  • /api/v1/tasks/7

Що таке {id} за змістом

id — це ідентифікатор. У межах нашого навчального домену найпростіше домовитися, що id — це ціле число (наприклад, int). Важливо саме домовитися й зафіксувати: клієнт має розуміти, що саме він підставляє в шлях.

Якщо не зафіксувати формат, почнеться хаос: один клієнт шле UUID, інший — числа, третій — 0007, а четвертий — "сім" (бо він оптиміст).

Приклад: функція, яка матеріалізує шлях із {id}

package main

import "fmt"

func taskPath(id int) string {
	return fmt.Sprintf("/api/v1/tasks/%d", id)
}

Такі функції здаються дрібницею, але вони дисциплінують. Коли ви пишете taskPath(7), ви не забудете /api/v1, не переплутаєте tasks із task і не зробите зайвий слеш. А ще такі функції потім дуже зручно використовувати в тестах: ви порівнюєте рядок, а не збираєте його вручну в кожному місці.

5. Стан done: поле і шлях /done

Із done зазвичай починається перша доросла дискусія про дизайн API. Чому? Тому що done — це наче просто булеве поле в задачі (done: true/false), але користувачеві хочеться окремої кнопки «Позначити виконаною», а розробнику — окремого ендпоінта, де все просто й зрозуміло.

У нашому домені ми хочемо підтримати зрозумілий сценарій: «позначити задачу виконаною». І в межах курсу ми беремо такий приклад шляху стану:

  • /api/v1/tasks/{id}/done

Це читається майже як фраза: «done для задачі {id}». Тут /done виступає як підресурс або «подання стану». Ми не робимо вигляд, що done — окрема сутність рівня /done/{id}. Ми тримаємо його усередині задачі.

Чому /done — це нормально

Раніше ми критикували «дієслова в URL» (/createTask). Але /done — інший випадок: це не команда до сервера, а частина моделі ресурсу — його стан. Ми ніби кажемо: «у задачі є ознака done, і для неї є окреме місце в шляху».

Можна думати так: /api/v1/tasks/7/done — це «ручка» стану виконаності для задачі 7.

Варіанти, які зазвичай порівнюють

Є два популярні підходи:

  1. Робити зміну через оновлення задачі (наприклад, часткове) і передавати {"done": true} у тілі.
  2. Робити окремий ендпоінт стану: /tasks/{id}/done.

У межах цієї лекції ми фіксуємо саме дизайн шляху /done. А от точну семантику методу (POST/PATCH) і деталі статусів краще тримати в межах загального контракту, але без глибокого занурення в реалізацію.

Приклад: функція шляху done для конкретної задачі

package main

import "fmt"

func taskDonePath(id int) string {
	return fmt.Sprintf("/api/v1/tasks/%d/done", id)
}

Тепер у вас є три стабільні «цеглинки»:

  • tasksPath() — колекція
  • taskPath(id) — один ресурс
  • taskDonePath(id) — стан done

Це і є «скелет» URL‑простору нашого API.

Невелика схема: як усе пов’язано

Коли в голові все це змішується, допомагає проста візуалізація. Тут немає жодної магії REST — лише дерево.

flowchart TD
    A["/api/v1/tasks
Колекція задач"] --> B["/api/v1/tasks/{id}
Одна задача"] B --> C["/api/v1/tasks/{id}/done
Стан done"]

Дерево показує важливу річ: /done живе під задачею, а не поруч із нею. Це знижує ризик помилок дизайну, де done раптом починає поводитися як окрема сутність без змісту.

6. Консистентність словника: tasks, done, {id}

Дуже часто API псують не складні баги, а дрібна неузгодженість. Один ендпоінт використовує /tasks, інший /todo, третій /items. У вашій голові це одне й те саме, а клієнт — не телепат. Він читає документацію буквально.

Тому в навчальному домені ми тримаємо один словник:

  • сутність: tasks
  • ідентифікатор: id
  • стан: done
  • версія: /api/v1

Якщо вам хочеться додати «краси» й назвати задачі todos, зупиніться й запитайте себе: «А що виграє клієнт?». Зазвичай відповідь одна: «нічого, окрім зайвого місця для помилки».

Тут корисна аналогія з іменами змінних у Go: якщо ви в одному місці пишете taskID, а в іншому id, а в третьому task_id, код стає важчим для читання. Дизайн URL — це те саме, тільки «змінні» читають інші програми.

7. Мапа API в Go-коді

Навіть до того, як у вас з’явиться реальний HTTP‑сервер, корисно тримати контракт у вигляді структури даних. Це допомагає не сперечатися на словах («ну там десь /done»), а бачити конкретні рядки.

І ще це зменшує шанс друкарських помилок. Друкарська помилка в URL — один із найприкріших багів: усе «майже правильно», але 404, і ви годину шукаєте зайву літеру.

Крок 1: заведемо операції та список маршрутів

package main

type Route struct {
	Method  string
	Pattern string
	Op      string
}

func apiRoutes() []Route {
	return []Route{
		{Method: "GET", Pattern: "/api/v1/tasks", Op: "list_tasks"},
		{Method: "POST", Pattern: "/api/v1/tasks", Op: "create_task"},
		{Method: "GET", Pattern: "/api/v1/tasks/{id}", Op: "get_task"},
		{Method: "POST", Pattern: "/api/v1/tasks/{id}/done", Op: "mark_done"},
	}
}

Зверніть увагу: ми використовуємо {id} саме як документаційний шаблон. Це зручно для читання й обговорення.

Крок 2: зробимо друк «мапи» як міні-документацію

package main

import "fmt"

func printRoutes(routes []Route) {
	for _, r := range routes {
		fmt.Printf("%s %-25s  # %s\n", r.Method, r.Pattern, r.Op)
	}
}

Крок 3: зберемо все в main

package main

func main() {
	printRoutes(apiRoutes())
}

Якщо запустити, вивід буде приблизно такий:

GET  /api/v1/tasks             # list_tasks
POST /api/v1/tasks             # create_task
GET  /api/v1/tasks/{id}        # get_task
POST /api/v1/tasks/{id}/done   # mark_done

Це виглядає майже як чернетка README. І в цьому його сила: контракт має бути простим, щоб уміщатися «на один екран».

Чому змінювати шляхи майже завжди боляче

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

У Go‑світі тема сумісності — не абстракція. Якщо публічний API змінюється, чужий код ламається, і це дуже практична проблема: не можна «забрати API назад» без поломок. HTTP‑контракт поводиться так само: ви не можете непомітно прибрати endpoint, якщо на нього хтось зав’язаний.

І є ще один хороший приклад того, як сумісність впливає на дизайн: коли в стандартній бібліотеці Go додавали context.Context, не можна було просто змінити сигнатуру http.Client.Do, бо це зламало б купу коду. Тому підтримку контексту вбудували в http.Request так, щоб не ламати старі виклики.

Це чудовий урок: дизайн інтерфейсу й API часто визначається не «ідеальною архітектурою», а відповідальністю перед користувачами.

8. Типові помилки під час дизайну шляхів задач

Помилка № 1: командні URL (/createTask, /markDone) замість ресурсів.
Зазвичай це трапляється через бажання зробити «як у RPC»: окрема команда на кожну дію. У підсумку у вас з’являється зоопарк шляхів, і клієнт не може передбачити API за формою. Лікується простим правилом: шлях — іменник (ресурс), дія — HTTP‑метод, а винятки на кшталт /done мають бути частиною моделі стану, а не випадковою командою.

Помилка № 2: відсутність єдиного словника (/tasks, /todo, /items) і дрейф назв.
Спочатку здається, що це дрібниця. Потім раптом виявляється, що різні команди розробки називали одне й те саме по-різному, і клієнт пише костилі «якщо /tasks не працює, спробуй /todo». Нормальний підхід — обрати один домен (tasks) і тримати його всюди: у шляхах, у DTO, у документації, у прикладах.

Помилка № 3: {id} не визначено як формат, і кожен клієнт розуміє його по-своєму.
Якщо ви не сказали, що id — це ціле число (або UUID), то клієнти почнуть вгадувати. Особливо весело, коли один клієнт шле "7", а інший шле "007", а третій шле "seven". У контракті потрібно явно зафіксувати: {id} — це, наприклад, додатне ціле число. Навіть якщо ви поки що не реалізували перевірку, домовленість уже має існувати.

Помилка № 4: плутанина між станом і ресурсом для done.
Іноді done раптом перетворюють на окремий ресурс верхнього рівня (/api/v1/done/{id}), і API стає дивним: «де тут задача, а де ознака?». У нашому домені done — це стан задачі, тому він живе під /tasks/{id}: /tasks/{id}/done. Так клієнту простіше зрозуміти, що змінюється саме стан конкретної задачі.

Помилка № 5: відсутність версії (/api/tasks замість /api/v1/tasks) і надія «ми завжди будемо акуратними».
Надія шляхетна, але реальність перемагає: API змінюється. Без версії вам або доведеться ламати старих клієнтів, або тягнути купу зворотної сумісності без явного розділення правил. Версія в шляху — простий і чесний спосіб сказати: «ось контракт, за який ми відповідаємо».

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