1. Створення як окрема операція
Читати те, що вже існує, — лише половина історії. Створення ресурсу — це момент, коли «чогось ще немає», але клієнт уже хоче, щоб «воно зʼявилося». У консольній Java ми часто просто робимо new і додаємо до списку, і здається, що життя прекрасне. У світі HTTP створення — це договір: клієнт описує, що він хоче створити, сервер вирішує, як саме це зʼявиться, і повертає результат так, щоб клієнт міг далі з ним працювати.
У домені ReadLater створення виглядає дуже приземлено: користувач хоче додати книгу до особистого списку читання. До цього в нього міг бути порожній список. Після створення в колекції зʼявляється новий елемент, у якого зʼявляється ідентифікатор, щоб потім його можна було прочитати, оновити або видалити, а також набір полів, наприклад назва та автор.
Уявіть звичайну ситуацію: ви приходите в кафе і кажете «Хочу лате та круасан». Ви не кажете: «Створіть замовлення номер 42». Номер замовлення призначає система кафе. У HTTP створення працює схожим чином: клієнт повідомляє склад замовлення, тобто дані майбутнього ресурсу, а сервер «пробиває чек» — тобто створює новий елемент і надає йому ідентифікатор.
Щоб це зробити передбачувано і без «внутрішньої телепатії», у HTTP є метод, який дуже часто використовують для створення нового елемента в колекції: POST.
2. POST і шлях колекції
Ресурсна пара тут уже знайома: колекція /reading-list і окремий елемент /reading-list/42. Для POST із цієї різниці потрібен один головний висновок: створювати логічно через шлях колекції, тому що в нового елемента ще немає власної адреси за id.
Тому типова форма створення виглядає так:
POST /reading-list
А не так:
POST /reading-list/42
У другому варіанті клієнт ніби намагається заздалегідь назвати id. На базовому рівні це зайва відповідальність: сервер сам створює елемент і призначає йому ідентифікатор. Бувають контракти, де клієнт приносить id сам, але для нашої прикладної моделі чесніше і зрозуміліше вважати, що id призначає сервер.
Ось коротка таблиця для самоперевірки:
| Що хоче клієнт | На що він націлюється | Чому |
|---|---|---|
| «Хочу отримати список» | /reading-list | потрібен набір |
| «Хочу отримати один елемент» | /reading-list/42 | потрібен конкретний елемент |
| «Хочу створити новий елемент» | /reading-list | новий елемент зʼявляється всередині набору |
Якщо коротко: POST — це розмова з колекцією. Ми стукаємо «у двері відділу кадрів», а не «до конкретного співробітника», якого ще навіть не найняли.
3. Тіло запиту POST
Шлях і метод відповідають на запитання «з яким ресурсом і якою дією ми працюємо». Але створення без даних зазвичай беззмістовне: щоб додати книгу до списку читання, потрібно хоча б зрозуміти, яку книгу ми додаємо.
Тому в POST найчастіше є body — те саме тіло запиту, яке ми вже бачили як частину HTTP request. Сьогодні нам не важливий конкретний формат тіла: ми не обговорюємо ані JSON, ані форми, ані кодування. Нам важлива ідея: клієнт передає опис майбутнього ресурсу.
На рівні домену ReadLater тіло запиту «людською мовою» може містити приблизно такі дані:
title: назва книги
author: автор
comment: коментар користувача (необовʼязково)
Можна уявити це як простий рядок — не як реальний протокол, а як ілюстрацію думки:
// Метод HTTP-запиту
String method = "POST";
// Шлях колекції: створення відбувається «всередині набору»
String path = "/reading-list";
// Тіло запиту: дані майбутнього ресурсу (у реальності формат буде інший, але ідея та сама)
String body = "title=Clean Code; author=Robert C. Martin; comment=Знайти паперове видання";
Зверніть увагу на важливий зсув мислення. Ми не надсилаємо «команду» на кшталт addBook. Ми надсилаємо опис ресурсу: «У моєму списку читання має зʼявитися запис із такими полями». Сервер уже сам вирішить, що саме зберігати, який видати ідентифікатор, які значення за замовчуванням застосувати і що повернути у відповідь.
Щоб трохи сильніше повʼязати це з Java-мисленням, можна уявити, що body — це «сировина» для створення обʼєкта. У консольній програмі ви могли б зібрати такі дані в обʼєкт, але зараз ми не привʼязуємося до формату передавання — просто показуємо, що для створення потрібен набір полів.
Наприклад, ми можемо уявити «на боці сервера» структуру даних, яку він хотів би отримати:
// Дані, необхідні для створення елемента списку читання.
// Це не про JSON/серіалізацію, а про «контейнер вхідних даних».
record CreateReadingItemData(String title, String author, String comment) {
}
Цей record тут не про JSON і не про серіалізацію, а просто про думку: створення — це набір даних, який надходить ззовні. У реальному HTTP ці дані надходитимуть у вигляді тексту в body, а вже потім сервер перетворюватиме їх на структуру.
4. Призначення ідентифікатора під час створення
У консольній Java дуже легко непомітно для себе змішати дві відповідальності: «я вирішив додати елемент» і «я сам вигадав, який у нього id». У бекенд-світі краще звикати до іншої картини: сервер — господар ресурсу, він відповідає за унікальність і за те, щоб ресурс існував як сутність системи, а не як фантазія клієнта.
Тому під час створення майже завжди є крок «сервер призначає id». Це можна показати на простому Java-прикладі без жодної мережі: ніби це шматок внутрішньої логіки сервера.
import java.util.concurrent.atomic.AtomicLong;
// Послідовність для генерації унікальних ідентифікаторів на боці сервера
AtomicLong idSequence = new AtomicLong(1);
// Сервер «видає» новий ID для створеного елемента
long newId = idSequence.getAndIncrement();
System.out.println("newId=" + newId); // newId=1
Чому це важливо саме в контексті POST? Тому що це пояснює, чому клієнт не має цілитися в /reading-list/42, коли створює елемент. Клієнт ще не знає, чи буде це 42, 43 або 100500 (і добре б, щоб це його взагалі не хвилювало).
Тепер зберемо це в маленьку історію. Клієнт надсилає POST /reading-list і «прикладає» дані. Сервер:
1. читає дані з body запиту;
2. створює новий елемент колекції;
3. призначає йому ідентифікатор;
4. повертає результат так, щоб клієнт міг далі адресувати його як окремий ресурс.
Ось проста схема процесу — без статусів і заголовків, вони будуть окремо пізніше, а зараз важлива лише логіка:
sequenceDiagram
participant C as Клієнт
participant S as Сервер
participant R as "Список читання (колекція)"
C->>S: POST /reading-list + body з даними про книгу
S->>R: add(newItem)
R-->>S: створений елемент з id=42
S-->>C: відповідь: створений елемент (зокрема з id=42)
Зверніть увагу, як акуратно «народжується» шлях окремого елемента: після створення зʼявляється сенс у /reading-list/42, тому що тепер є ресурс, який можна адресувати.
5. Відповідь після POST
Після створення клієнту потрібно зрозуміти дві речі: по-перше, чи вдалося створення взагалі, по-друге, що саме вдалося. Навіть якщо клієнт і так знає, що надсилав, сервер може застосувати якісь правила, доповнити дані, нормалізувати їх або просто призначити ідентифікатор, якого раніше не було.
Тому типова коректна відповідь після POST містить представлення створеного ресурсу. У нашому домені це міг би бути текст на кшталт «створено елемент списку читання з id=42, title=..., author=...». У реальному API це буде структуроване тіло відповіді, але ми зараз не обговорюємо формат.
На рівні базової ментальної моделі корисно думати так: POST — це «заявка на створення», а відповідь сервера — це «квитанція», де є номер і підсумкові дані.
Ось мініприклад на Java, який ілюструє ідею «сервер повернув те, що створив» — це не HTTP-код, а логіка:
// ID призначено сервером під час створення
long createdId = 42L;
// Сервер може повернути підсумкові дані ресурсу (включно з тими, що надійшли від клієнта)
String createdTitle = "Clean Code";
String createdAuthor = "Robert C. Martin";
// Клієнт отримує «квитанцію»: id + підсумкові поля створеного ресурсу
System.out.println("Створено: id=" + createdId + ", title=" + createdTitle);
// Створено: id=42, title=Clean Code
Найважливіше тут — не формат, а сенс: після POST зʼявляється новий елемент, і клієнт отримує можливість звертатися до нього як до окремого ресурсу.
Якщо хочеться дуже коротко повʼязати GET і POST в один «потік», то виходить така логіка:
POST /reading-list створює елемент і повертає його (з id).
GET /reading-list/id потім дає змогу прочитати цей елемент, використовуючи виданий ідентифікатор.
Ми поки не обговорюємо, який саме статус буде повернено і які заголовки будуть важливі. Тут нам потрібен сам принцип: після POST клієнт має побачити, що ресурс зʼявився і як до нього звернутися далі. Числові статуси, Location та інша семантика відповіді — це вже окремий шар HTTP-контракту; без цього створення легко перетворюється на «чорну діру».
6. POST і ресурсний шлях
На цьому етапі достатньо одного нагадування: POST /reading-list читається краще, ніж /addBookToReadingList, тому що шлях продовжує називати ресурс, а дію виражає сам метод. Якщо сховати створення в адресі, API знову скочується в набір команд.
Для звичайного CRUD-сценарію цього принципу достатньо: є колекція, після створення в елемента зʼявляється власна адреса, а методи показують, що саме ми з цим ресурсом робимо.
Повтори POST і дублікати
У POST є одна дуже практична незручність: повтор того самого запиту може створити другий такий самий елемент. Це не вада методу, а нормальна семантика створення — кожен POST знову просить додати ресурс до колекції.
Тут достатньо запамʼятати сам ризик дублікатів: якщо клієнт, UI або мережа повторять створення, результат може дублюватися сильніше, ніж ви очікували. Тому запитання «що буде при повторі запиту?» для HTTP-методів узагалі не декоративне.
7. Типові помилки під час роботи з POST
Помилка №1: надсилати POST на шлях окремого ресурсу з «вигаданим» id.
Це виглядає логічно, якщо ви подумки хочете контролювати все, але зазвичай ламає базову модель: клієнт не має вгадувати ідентифікатор. У ресурсному мисленні створення відбувається в колекції, а конкретний id зʼявляється як результат. Якщо дуже хочеться керувати id, це має бути окреме усвідомлене рішення контракту, а не «ну я так відчуваю».
Помилка №2: намагатися виразити створення лише шляхом, без тіла запиту.
Початківці іноді пишуть щось на кшталт POST /reading-list/clean-code, сподіваючись, що «адреса сама все скаже». Це швидко перетворює API на дивний набір правил. Для створення зазвичай потрібні дані: назва, автор, коментар, статус і т.д. Шлях має залишатися простим і стабільним, а дані — надходити в body.
Помилка №3: перетворювати POST на універсальний метод «на все, що незрозуміло».
Якщо у вас є звичка «не знаю, що робити — надішлю POST», API виходить туманним. Клієнтові важко передбачати поведінку, а серверу — підтримувати контракт. Навіть без глибокого знання інших методів уже корисно тримати в голові просту думку: POST — це насамперед створення нового елемента в колекції, а не «чарівна кнопка».
Помилка №4: не думати про результат створення для клієнта.
Іноді сервер «створює щось усередині», але клієнтові повертає порожнечу або невиразний текст. У результаті клієнт не знає, що зʼявилося, який у ресурсу ідентифікатор і як до нього звернутися далі. Навіть не заглиблюючись у статуси й заголовки, корисно тримати правило: після створення клієнт має отримати достатньо інформації, щоб продовжити роботу з новим ресурсом.
Помилка №5: не помічати ризик дублікатів під час повторного надсилання запиту.
Якщо ви подумки вважаєте POST чимось на кшталт «встановити значення», ви здивуєтеся, коли повтор створить другий елемент. Це не «пастка HTTP», а нормальна семантика створення. Важливо заздалегідь прийняти цю модель, щоб потім не проєктувати API і клієнтську поведінку на хибному припущенні «повтори завжди безпечні».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ