1. Параметри запиту як частина контракту
Коли ви вперше бачите /api/v1/tasks?done=true&limit=20, легко подумати: «Та це ж просто рядок після ?, яка тут різниця». Різниця величезна: параметри запиту — це спосіб налаштувати видачу, не змінюючи сам ресурс. Тобто ресурс лишається «списком задач», але ми кажемо серверу, який саме список нам потрібен і як його показати.
У ресурсу списку задач /api/v1/tasks майже завжди є три природні «потреби клієнта».
Перша — відфільтрувати список: показати лише виконані або лише невиконані, знайти за підрядком, вибрати задачі з певними тегами. Друга — відсортувати: за id або за назвою, щоб не дивуватися, чому сьогодні порядок один, а завтра інший. Третя — обмежити обсяг: отримати 20 задач замість 2000, тобто застосувати пагінацію.
Важливо зафіксувати правильну філософію: параметри запиту керують представленням списку, але не замінюють ідентифікацію ресурсу. Якщо потрібно обов’язково вказати, «яку саме задачу», то це зазвичай шлях (/api/v1/tasks/{id}), а не query. Query — це «налаштування видачі».
2. Словник параметрів для /api/v1/tasks
Коли починаєш додавати параметри по одному, легко отримати хаос: сьогодні done, завтра isDone, післязавтра completed, і клієнту доводиться мучитися з копіпастою. Тому ми робимо дорослу річ: фіксуємо набір параметрів як словник контракту для лістингу задач. Він має бути коротким, стабільним і не намагатися «впхати весь SQL в один рядок».
Нижче — базовий контракт, достатній для навчального проєкту й майже для будь-якого простого реального API. Ми свідомо тримаємо його мінімальним: що менше параметрів, то простіша підтримка і менше сюрпризів.
| Параметр | Тип (після парсингу) | Що означає | Значення за замовчуванням (якщо параметр не передано) | Приклад |
|---|---|---|---|---|
|
(але параметр необов’язковий) |
Фільтр за статусом виконання | фільтр не застосовується | |
|
|
Пошук за текстом (наприклад, у полі title) | порожній пошук (без фільтрації) | |
|
(повторюваний ключ) |
Фільтр за тегами | фільтрація за тегами не застосовується | |
|
enum (, ) |
За яким полем сортуємо | |
|
|
enum (, ) |
Напрямок сортування | |
|
|
|
Скільки елементів повернути | |
|
|
|
З якого елемента почати | |
|
Є два важливі змістові застереження.
По-перше, done — необов’язковий фільтр. Якщо done не задано, це не означає done=false; це означає «покажи все». Ми ще повернемося до цієї думки, бо це джерело класичних багів.
По-друге, tag — повторюваний параметр, і це нормальний спосіб передати список у запиті. Не саморобний CSV в одному рядку, а саме tag=go&tag=http.
3. Нюанси проєктування параметрів
Найменування параметрів
У назвах API-параметрів найчастіше помиляються не компілятори, а люди. Параметр назвали один раз, клієнт вшив його в мобільний застосунок, і раптом виявилося, що перейменувати його «безболісно» вже не можна. Тому до неймінгу параметрів запиту варто ставитися як до публічного інтерфейсу: нудно, акуратно, передбачувано.
Тут працює просте правило: ім’я параметра має бути коротким, зрозумілим і без фантазій. Ми використовуємо нижній регістр і прості слова: done, q, tag, sort, order, limit, offset. Це не самовираження, а контракт.
Окремо про q. Іноді в новачків виникає думка: «А чому не query?» Можна і query, але короткий q — звичний патерн для пошукових форм і багатьох API. Головне — обрати один варіант і дотримуватися його.
Про багатослівні імена. Якщо вони все-таки потрібні, краще дотримуватися snake_case (наприклад, created_at, due_before), але в поточній версії контракту ми свідомо тримаємо лише однослівні параметри, щоб не ускладнювати.
Фільтри
Фільтри — це момент, коли користувач каже: «Мені потрібен не весь список задач, а підмножина». І тут легко випадково зробити API, яке начебто працює, але його неможливо пояснити й протестувати. Тому фільтри мають бути простими та комбінованими.
Найпростіша й найзрозуміліша модель комбінування — усі фільтри застосовуються одночасно, тобто логічне AND.
Наприклад, запит:
/api/v1/tasks?done=false&q=milk&tag=home&tag=shopping
розумно читати так: «Дай мені невиконані задачі, де текст містить milk, і які мають теги home та shopping». Можна сперечатися, чи мають теги означати AND чи OR, і саме це — частина контракту, яку важливо зафіксувати словами.
Для навчального проєкту зручно прийняти правило: якщо передано кілька tag, то задача підходить, якщо містить принаймні один із тегів (АБО всередині тегів), а щодо інших фільтрів усе одно І. Виходить: «фільтр done І пошук І (tag1 АБО tag2 АБО ...)». Це легше пояснити користувачу і простіше реалізувати.
У межах цієї лекції ми не пишемо код фільтрації, але вже проєктуємо поведінку так, щоб вона була передбачуваною.
Сортування
Сортування здається простою справою, поки ви не зрозумієте, що «за замовчуванням» — це теж сортування, тільки неявне. А неявне сортування зазвичай означає «як пощастить»: сьогодні база віддала в одному порядку, завтра — в іншому, а тести клієнта починають поводитися нестабільно.
Тому для списку задач має бути визначений детермінований порядок за замовчуванням. У нашому контракті це sort=id і order=asc. Чому id? Тому що це найстабільніше й однозначне поле в базовому CRUD.
Також корисно подумки домовитися про tie-breaker, тобто вторинний ключ сортування. Навіть якщо ми сортуємо за title, у двох задач може бути однакова назва. Тоді порядок усе одно має бути стабільним — зазвичай другим ключем стає id. Це деталь реалізації, але її корисно тримати в голові: інакше сортування за title буде стрибати, і користувач думатиме, що API живе власним життям.
Параметри sort і order проєктуються парою: sort відповідає на запитання «за чим», order — «у якому напрямку». Це простіше, ніж намагатися втиснути в один параметр щось на кшталт sort=title_desc.
Пагінація
Пагінація — це не лише зручність інтерфейсу, а й захист сервера та клієнта від випадкового «вивантажте мені всі задачі за весь час». Якщо хтось колись робив SELECT * без LIMIT по таблиці на мільйон рядків, він потім починає поважати пагінацію майже релігійно, і це нормально.
Ми використовуємо просту модель limit/offset. Читається вона так: offset — скільки пропустити з початку відсортованого списку, limit — скільки повернути після цього.
Наприклад: /api/v1/tasks?sort=id&order=asc&limit=20&offset=40
означає: «Поверни задачі з 41-ї по 60-ту в порядку зростання id». І одразу важлива думка: пагінація має сенс лише разом із фіксованим сортуванням. Інакше «сторінки» можуть перемішуватися.
Контрактом потрібно зафіксувати розумні обмеження. Наприклад, limit за замовчуванням 20, а максимальний limit — 100. Це не примха викладача, а нормальна практика: якщо клієнту раптом потрібно вивантажити 5000 задач, це окремий сценарій, і його треба робити свідомо.
Повторювані параметри
Повторювані параметри запиту інколи виглядають дивно для тих, хто звик передавати масиви в JSON. Але в URL це природний патерн: один ключ може мати кілька значень, бо query string за своєю природою ближчий до map[string][]string, а не до map[string]string.
Тобто tag=go&tag=http — це не хак і не костиль, а стандартний спосіб виразити список. Ви побачите його в найрізноманітніших системах: від простих REST API до складних пошукових запитів.
З практичного боку це дає приємну властивість: якщо клієнт хоче додати тег, він не повинен парсити CSV-рядок, акуратно екранувати коми й думати, що робити з тегом dev,ops. Він просто додає ще один tag.
У стандартній бібліотеці Go це саме так і моделюється: під час розбору query ви отримуєте структуру, де ключ може мати кілька значень, а код потім бере або перше значення, або всі одразу. Навіть у прикладах, де з query беруть одне значення через r.URL.Query().Get("guess"), мається на увазі, що query — це набір параметрів, який сервер читає структуровано, а не шляхом розбиття за символом &.
4. Приклади запитів до /api/v1/tasks
Коли контракт зафіксовано, стає простіше «думати URL-ами». Це корисна навичка: ви дивитеся на посилання і розумієте, що сервер має зробити, без ворожіння та магії.
Ось кілька прикладів і те, як їх правильно інтерпретувати.
package main
import "fmt"
func main() {
fmt.Println("/api/v1/tasks") // список задач без фільтрів; sort=id, order=asc, limit=20, offset=0
}
Тут узагалі немає query. І це нормально: значення за замовчуванням — частина контракту, і відсутність параметрів має мати чітке значення.
package main
import "fmt"
func main() {
fmt.Println("/api/v1/tasks?done=true") // лише виконані, решта параметрів за замовчуванням
}
done=true застосовує фільтр, але не змінює сортування чи пагінацію.
package main
import "fmt"
func main() {
fmt.Println("/api/v1/tasks?q=milk&tag=home&tag=shopping") // пошук і фільтр за тегами
}
Ми читаємо це так: «пошук за milk і фільтр за тегами». Як саме трактувати кілька тегів — АБО чи І — ми заздалегідь фіксуємо в контракті, а не як вийде.
package main
import "fmt"
func main() {
fmt.Println("/api/v1/tasks?sort=title&order=desc&limit=10&offset=20") // сортування й пагінація
}
Тут запит уже схожий на повноцінний клієнтський лістинг: відсортували за назвою за спаданням і взяли сторінку.
5. Модель параметрів у коді
Тут хочеться одразу перейти до парсингу limit через strconv.Atoi і валідації sort через список дозволених значень. Але за нашим планом дня це буде наступними кроками. Зараз ми робимо рівно одну корисну річ: проєктуємо тип у коді, який зберігає типізовані параметри лістингу.
Це допомагає не змішувати читання параметрів і бізнес-логіку списку задач. Ми хочемо прийти до схеми: спочатку розпарсили й провалідовували вхід, отримали структуру параметрів, і лише потім передали її у функцію, яка вміє відбирати задачі.
package main
type TaskListQuery struct {
// Done: nil = фільтр не передано; true/false = фільтр передано.
Done *bool
Q string // рядок пошуку
Tags []string // повторювані tag=...
Sort string // "id" або "title"
Order string // "asc" або "desc"
Limit int
Offset int
}
Зверніть увагу на *bool. Це не любов Go до вказівників, а спосіб чесно зберігати три стани: «не передано», «true», «false». Якщо зробити просто bool, ви не відрізните done=false від «done не надіслали». А це вже інший зміст.
Тепер задамо значення за замовчуванням. Ними зручніше керувати централізовано, а не розкидати 20 і id по коду, як конфеті.
package main
func DefaultTaskListQuery() TaskListQuery {
return TaskListQuery{
Done: nil,
Q: "",
Tags: nil,
Sort: "id",
Order: "asc",
Limit: 20,
Offset: 0,
}
}
І тепер у нас з’являється чітка межа: парсер параметрів запиту має взяти DefaultTaskListQuery(), застосувати те, що прийшло від клієнта, і або повернути готову структуру, або помилку валідації. Саму реалізацію парсингу ми робитимемо пізніше, і там якраз з’являться url.Values, Get, списки значень тощо.
До речі, у Go стандартна бібліотека показує, що «розбір query → отримання рядка → конвертація → валідація» — це абсолютно нормальний шлях: спочатку ви берете рядкове значення з query, потім перетворюєте його на число й перевіряєте межі. А якщо потрібно розібрати query як окремий рядок, без повного URL, для цього теж є стандартний інструмент: розбір query-рядка з подальшим Encode() трапляється навіть у прикладах тестування.
6. Типові помилки під час проєктування параметрів запиту
Помилка № 1: робити обов’язкові параметри в query.
Якщо клієнт зобов’язаний передати id як ?id=123, ви майже напевно ускладнюєте життя: це вже ідентифікація ресурсу, а не налаштування видачі. Ідентифікація живе в шляху, а query залишають для фільтрів, сортування й пагінації.
Помилка № 2: не фіксувати значення за замовчуванням і «сподіватися на здоровий глузд».
Без значень за замовчуванням контракт перетворюється на ворожіння: клієнт не знає, скільки елементів повернеться, у якому порядку й що означає відсутність параметра. Значення за замовчуванням — це не дрібниця, а частина публічної поведінки API.
Помилка № 3: плутати «параметр відсутній» і «параметр дорівнює false/0/порожньо».
Класика — done=false проти відсутнього done. Якщо ви зберігаєте done як bool, у вас неминуче з’явиться логіка: а як зрозуміти, чи його передали. У підсумку код починає вигадувати евристики, і баги з’являються раптово.
Помилка № 4: приймати будь-які значення sort і «розрулювати потім».
Якщо сервер мовчки приймає sort=banana, а потім або падає, або сортує якось за замовчуванням, клієнт не розуміє, що зробив не так. Для переліків потрібен список дозволених значень — інакше контракт розпливається.
Помилка № 5: не обмежувати limit і не думати про навантаження.
limit=1000000 — це не «користувач захотів», а потенційна атака або, принаймні, випадкове перевантаження. Навіть у навчальному проєкті корисно мислити як інженер: ставити розумні межі й пояснювати їх помилкою валідації.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ