1. HTTP як контракт: запит → відповідь
Коли люди вперше чують «HTTP», вони часто думають: «Ну це ж про сайти». І так, сайти — теж. Але для розробника HTTP — насамперед дуже строгий спосіб домовитися між двома програмами: одна надсилає запит (client), інша відповідає (server). Цю домовленість називають контрактом. Вона охоплює не лише «який URL смикнути», а й «який метод», «які коди успіху й помилок», «який формат тіла» та «що вважається коректними вхідними даними».
У Go це особливо приємно: мова історично дуже сильна для серверів і мережевого коду. Навіть у матеріалах про еволюцію Go підкреслюють, що її проєктували саме для написання серверів.
Ментальна модель «конверта»
Уявіть, що HTTP — це пошта.
- Лист = запит (request).
- Конверт = метадані (метод, шлях, заголовки).
- Вміст листа = тіло запиту (наприклад, JSON).
- Лист у відповідь = відповідь (response).
- Штамп на відповіді = статус-код (200, 404, 500…), який коротко й однозначно говорить, що сталося.
Звучить трохи олдскульно, але як ментальна модель це працює ідеально: сервер не вгадує настрій, а відповідає формально.
Схема запиту та відповіді
sequenceDiagram
participant C as Клієнт
participant S as Сервер
C->>S: HTTP-запит (method + path + headers + body)
S->>S: перевірка + обчислення
S-->>C: HTTP-відповідь (status + headers + body)
Найважливіше тут ось що: на виході завжди є статус-код, а тіло відповіді може бути, а може й не бути. Клієнт має вміти орієнтуватися на статус-код, а не намагатися «прочитати помилку очима».
Endpoint: метод + шлях
Щоб перестати говорити про HTTP як про магію, зручно ввести маленький термін: endpoint — це конкретна точка входу в API. Він складається з двох частин: HTTP-метода (що робимо) і шляху (з чим працюємо).
Наприклад, «отримати список задач» часто виглядає так: GET /api/v1/tasks.
Зараз ми не запускаємо сервер і не пишемо обробники. Ми робимо те, що роблять досвідчені інженери на початку проєкту: фіксуємо контракт. І для цього можна навіть написати пару структур у Go — не тому, що нам потрібен код, а щоб думки стали точнішими.
Мінімальний тип для опису 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"}
Тут важлива не краса коду, а дисципліна мислення: endpoint — це саме пара (method, path). Якщо ви змінюєте метод, ви змінюєте зміст. Якщо змінюєте шлях, ви змінюєте ресурс.
2. HTTP-методи: зміст дії та обіцянки клієнту
Про методи зручно думати як про дієслова з дуже строгими правилами. Якщо використовувати методи як заманеться, клієнт змушений вгадувати поведінку, а API перетворюється на квест «вгадай кнопку». Якщо ж ви дотримуєтеся стандартної семантики, клієнт і інші розробники розуміють поведінку без перегляду вихідного коду сервера. Це економить години, нерви й безліч повідомлень: «А чому тут 200, якщо це помилка?».
Нижче — практична таблиця, яку насправді тримають у голові, коли проєктують API.
Таблиця базової семантики методів
| Метод | Ідея простими словами | Зазвичай для чого | Ідемпотентність |
|---|---|---|---|
|
«Дай дані» | читання | так |
|
«Створи / виконай дію» | створення/команда | зазвичай ні |
|
«Замінити повністю» | повна заміна ресурсу | так |
|
«Змінити частково» | часткова зміна | зазвичай так (якщо спроєктовано акуратно) |
|
«Видали» | видалення | так |
Кілька уточнень, щоб це не здавалося сухою теорією.
GET майже завжди безпечний у тому сенсі, що не має змінювати стан на сервері. Якщо GET раптом створює задачу, списує гроші й надсилає лист мамі — це не GET, а сюжет для фільму жахів.
POST часто використовують для створення: «створити задачу», «створити користувача». Але іноді POST — це команда, яка не вкладається в «замінити стан». Наприклад: «запустити перерахунок звіту». У нашому навчальному домені задач ми триматимемо POST як «створити задачу», бо так простіше.
PUT і PATCH: якщо ви не впевнені, не поспішайте їх застосовувати. Але розуміти зміст важливо: PUT — це «ось нова версія ресурсу повністю», PATCH — «ось маленька зміна».
DELETE — це видалення. І тут починається філософія: що означає «видалити», якщо ресурс уже видалено? Зазвичай домовляються так, щоб повторне виконання DELETE не ламало систему.
3. Статус-коди: мова відповіді сервера без вгадування за текстом
Якщо метод — це «що ми хочемо», то статус-код — це «що вийшло». І це не другорядна деталь, а частина контракту: клієнт пише свою логіку, орієнтуючись на коди. У хорошому API клієнту не потрібно читати рядок помилки й здогадуватися, що ви мали на увазі. Він бачить 404 і розуміє: «не знайдено». Він бачить 400 і розуміє: «я щось надіслав не так».
Тут є дуже зручна ментальна модель: класи кодів.
- 2xx — успіх,
- 4xx — проблема в запиті (клієнт винен або принаймні може це виправити),
- 5xx — проблема на сервері (клієнт не винен).
Невелика функція «клас коду»
package main
func statusClass(code int) int {
return code / 100
}
// 200 -> 2
// 404 -> 4
// 500 -> 5
Так, це шкільна математика. Але вона допомагає: побачили statusClass == 4 — значить, це помилка запиту. Побачили statusClass == 5 — отже, щось зламалося в нас.
Мінімальний набір кодів, які потрібно впевнено розуміти
У межах нашого майбутнього API для задач нам вистачить невеликої базової п’ятірки, а також ще одного коду:
| Код | Назва | Коли зазвичай використовують | Чи є тіло відповіді |
|---|---|---|---|
|
OK | успішне читання/зміна | зазвичай так |
|
Created | ресурс створено | зазвичай так (часто повертають створений обʼєкт) |
|
No Content | успіх без тіла | ні (і це важливо!) |
|
Bad Request | некоректні вхідні дані / формат / валідація | зазвичай так (помилка в JSON) |
|
Not Found | ресурс не знайдено | зазвичай так |
|
Internal Server Error | внутрішня помилка сервера | зазвичай так (але повідомлення стабільне й безпечне) |
Критичний нюанс щодо 204: це не «200, але без настрою». Це явний сигнал: «успіх, тіла немає». Якщо ви повернете 204 і водночас надішлете JSON, деякі клієнти просто його проігнорують, а деякі поводитимуться непередбачувано. Тобто ви самі собі створите баг, а потім героїчно його виправлятимете.
Швидка функція «це успіх?»
package main
func is2xx(code int) bool {
return code >= 200 && code <= 299
}
4. Ідемпотентність: повторили запит — світ не став гіршим
Слово «ідемпотентність» звучить так, ніби його вигадали, щоб лякати студентів на контрольних. На практиці ідея дуже проста: якщо ви повторите запит, підсумковий ефект не має змінитися. Це не означає, що «нічого не змінюється». Це означає: повтор не додає зайвого ефекту.
Приклад із життя: кнопка «видалити задачу». Якщо користувач натиснув її двічі (або мережа смикнулася, і клієнт повторив запит), задача має залишитися видаленою, а не «видалитися двічі» (що б це не означало).
Ідемпотентність важлива через реальний світ: мережа нестабільна, зʼєднання рвуться, клієнт може не отримати відповідь і вирішити повторити запит. Якщо ви проєктуєте API так, щоб повтор був безпечним, ви зменшуєте кількість дивних дублів і «примарних» станів.
Грубе правило для початку
Часто вважають, що GET/PUT/PATCH/DELETE мають бути ідемпотентними, а POST — ні (бо «створити» двічі = два ресурси). Це не абсолютна істина, але добра стартова дисципліна.
package main
func isIdempotent(method string) bool {
switch method {
case "GET", "PUT", "PATCH", "DELETE":
return true
default:
return false
}
}
Маленька демонстрація, щоб відчути це на практиці
package main
import "fmt"
func main() {
fmt.Println(isIdempotent("GET")) // true
fmt.Println(isIdempotent("POST")) // false
fmt.Println(isIdempotent("DELETE")) // true
}
Це, звісно, не «справжня безпека API». Але це хороший спосіб перестати плутати ідемпотентність із «нічого не змінює».
5. Прив’язуємо до домену: чернетка HTTP-контракту для задач
Зараз ми зробимо важливий крок: переведемо абстрактні HTTP-слова в конкретний словник нашого застосунку. Ми вже давно живемо в домені задач (tasks): додаємо задачу, переглядаємо список, позначаємо її виконаною. У HTTP-стилі нам важливо, щоб шлях описував сутність, а метод — дію.
Домовимося, що API ми версіюємо через шлях. Це виглядає так: /api/v1/.... Версія в шляху — це спосіб сказати: «контракт зафіксовано, і якщо ми його зламаємо, то зробимо це в іншій версії, а не зненацька в пʼятницю ввечері».
Таблиця endpointів як чернетка контракту
| Зміст операції | Метод | Шлях | Успіх |
|---|---|---|---|
| Список задач | |
|
|
| Створити задачу | |
|
|
| Отримати задачу | |
|
|
| Позначити «done» | |
|
200 (або 204, якщо вирішимо обійтися без тіла) |
| Видалити задачу | |
|
|
Зверніть увагу: ми поки не обговорюємо JSON-формати та єдиний формат помилок (це теж частина контракту, але окрема тема). Тут ми тренуємо скелет: метод, шлях, статус успіху — щоб у нас не було POST /getTask і GET /deleteTask. Ми пишемо API так, щоб його можна було читати майже як англійську (ну або як дуже дивну англійську).
Трохи коду, щоб зафіксувати методи й не писати рядки вручну
Рядки "GET" і "POST" працюють, але в них легко помилитися. Тому навіть на ранньому етапі корисно завести константи.
package main
type Method string
const (
MethodGet Method = "GET"
MethodPost Method = "POST"
MethodPut Method = "PUT"
MethodPatch Method = "PATCH"
MethodDelete Method = "DELETE"
)
А тепер endpointи вже можна описувати «типізовано»:
package main
type Endpoint struct {
Method Method
Path string
}
var listTasks = Endpoint{Method: MethodGet, Path: "/api/v1/tasks"}
var deleteTask = Endpoint{Method: MethodDelete, Path: "/api/v1/tasks/{id}"}
6. Типові помилки під час проєктування HTTP-API
Помилка №1: змінювати стан через GET.
Зазвичай це стається «для зручності»: «ну я ж просто відкрию посилання…». Але GET — це читання. Якщо GET змінює стан, ви ламаєте кешування, повторні запити й дивуєте всіх клієнтів. У якийсь момент ви зловите баг «у нас задача створюється сама» — і це буде не магія, а GET.
Помилка №2: завжди відповідати 200 і ховати помилки в тексті.
Іноді роблять так: статус 200, а всередині JSON — поле "error": "...". Для клієнта це жахливо: йому потрібно парсити тіло, щоб зрозуміти, успіх це чи ні. Правильніше, щоб успіх був 2xx, помилка запиту — 4xx, а внутрішня помилка сервера — 5xx. Тіло помилки може бути структурованим, але код — первинний.
Помилка №3: плутати 400 і 404.
Якщо клієнт надіслав id = "abc" і ви не можете розібрати його як число — це не «не знайдено», а «некоректний запит», тобто 400. А 404 — це коли id коректний, але такого ресурсу немає. Ця різниця сильно спрощує життя клієнтам і тестам.
Помилка №4: віддавати тіло відповіді при 204.
204 означає «успіх, але тіла немає». Якщо ви все ж надішлете JSON, частина клієнтів його проігнорує. Вийде API, яке «іноді працює». А «іноді працює» — це особливий вид болю, бо дебажити нічого: воно ж «іноді».
Помилка №5: не думати про ідемпотентність і повтори.
У реальному світі запити повторюються. Якщо ви проєктуєте операції так, що повтор DELETE раптово перетворюється на 500, або повтор PATCH створює дублювальні зміни, то клієнти боятимуться ретраїв, а ви ловитимете «дивні» стани. Ідемпотентність — це не прикраса, а страховка від мережевої реальності.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ