Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Яка різниця між PUT i PATCH?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**PUT vs PATCH**: PUT повністю замінює ресурс; PATCH оновлює тільки вказані поля. Поле, якого немає в PUT-запиті, перезаписується null або дефолтом. ```http PATCH /users/123 {"email": "new@mail.com"} // phone залишається незмінним ``` **Ключове:** великий ресурс, мало змін - PATCH. Повна заміна стану - PUT.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**PUT vs PATCH** - PUT повністю замінює ресурс тілом запиту; PATCH оновлює тільки вказані поля. ## Теорія ### TL;DR - PUT = повна заміна: поля, яких немає в тілі, перезаписуються null або дефолтом зі схеми - PATCH = часткове оновлення: змінюються тільки вказані поля, решта залишається - Аналогія: PUT - це новий паспорт замість старого; PATCH - наклейка поверх чинного - Ідемпотентність: PUT завжди, PATCH - не гарантовано, залежить від реалізації - Правило: клієнт тримає повний стан, ресурс невеликий → PUT. Великий ресурс, мало змін → PATCH ### Короткий приклад ```http // PUT /users/123 - замінює ВЕСЬ об'єкт PUT /users/123 Content-Type: application/json { "id": 123, "name": "Аліса", "email": "alice@new.com", "phone": null } // Якщо раніше phone: "123-456", тепер він null // PATCH /users/123 - оновлює ТІЛЬКИ email, phone не чіпає PATCH /users/123 Content-Type: application/json { "email": "alice@new.com" } // phone: "123-456" залишається без змін ``` PUT перезаписує незазначені поля. PATCH залишає їх як є. ### Головна різниця PUT сприймає тіло запиту як новий повний стан ресурсу. Все, чого немає в тілі, замінюється null або дефолтом зі схеми (RFC 7231). PATCH застосовує diff: змінюються тільки вказані поля, решта залишається недоторканою. Це визначено в RFC 5789. Практичний наслідок: необережний PUT знищує дані без жодного повідомлення про помилку. ### Коли що використовувати - Клієнт контролює весь ресурс і він невеликий → PUT (перезапис конфігу, CRUD-форма де завжди є всі поля) - Ресурс із багатьма полями, змінити потрібно одне-два → PATCH (профіль користувача, платіжні дані) - Клієнт не може спочатку забрати повний стан → PATCH (економія трафіку) - Потрібен ідемпотентний повний перезапис → PUT - Можливі конкурентні оновлення → PATCH із JSON Merge Patch (RFC 7396) або JSON Patch (RFC 6902) ### Таблиця порівняння | Аспект | PUT | PATCH | |---|---|---| | **Тіло запиту** | Повне представлення ресурсу | Тільки зміни | | **Незазначені поля** | Перезаписуються (null або дефолт) | Зберігаються | | **Ідемпотентність** | Так | Не гарантовано | | **Content-Type** | application/json | application/json-patch+json або application/merge-patch+json | | **RFC** | RFC 7231 | RFC 5789 | | **Коли використовувати** | Повна заміна стану, малі ресурси | Рідкі оновлення, великі ресурси | ### Як сервер обробляє кожен метод При отриманні PUT сервер повністю замінює поточний запис і зберігає нове тіло. Поля, яких немає в запиті, отримують null або дефолти зі схеми. Саме тому PUT без усіх полів втрачає дані без жодного попередження. Для PATCH сервер застосовує тільки вказані ключі. Бібліотека `fast-json-patch` у Node.js реалізує це через операції: `{"op": "replace", "path": "/email", "value": "new@mail.com"}` змінює тільки цей вузол у JSON-дереві. ### Типові помилки **Помилка 1: PUT для часткового оновлення** ```javascript // Неправильно: PUT без phone - сервер ставить phone = null fetch('/users/1', { method: 'PUT', body: JSON.stringify({ name: 'Аліса' }) }); // Результат: phone стерто. Без помилки, без попередження. // Виправлення: використовуй PATCH або спочатку забери повний об'єкт і відправ злитий результат ``` **Помилка 2: не ідемпотентний PATCH-обробник** ```javascript // Неправильно: інкремент при кожному виклику, повторний запит подвоїть лічильник app.patch('/items/:id', (req, res) => { item.count += req.body.amount; }); // Правильно: пряма заміна значення - однаковий результат при кожному повторі app.patch('/items/:id', (req, res) => { item.count = req.body.count; }); ``` **Помилка 3: неправильний Content-Type для PATCH** ```javascript // Неправильно: деякі сервери сприймають application/json як повний PUT headers: { 'Content-Type': 'application/json' } // Правильно для масиву JSON Patch операцій: headers: { 'Content-Type': 'application/json-patch+json' } // Правильно для merge patch (diff-об'єкт, null означає видалення): headers: { 'Content-Type': 'application/merge-patch+json' } ``` **Помилка 4: PUT без If-Match у конкурентному середовищі** Сліпий PUT перезаписує зміни іншого клієнта, зроблені між твоїм read і write. Додай ETag і надсилай `If-Match: "abc123"` разом із PUT. Якщо ресурс змінився, сервер поверне 412 Precondition Failed і змусить знову забрати актуальну версію. ### Де це зустрічається - **Firebase**: `update()` = поведінка PATCH (зберігає поля), `set()` = поведінка PUT - **Stripe API**: `PATCH /customers/{id}` для рідких оновлень білінгу - **Strapi CMS**: JSON Patch для часткових оновлень контенту - **React Query / SWR**: `useMutation` із PATCH для оптимістичного UI - **Express.js + MongoDB**: `replaceOne` для PUT (повна заміна), `updateOne` з `$set` для PATCH ### Додаткові питання **Q:** Чому PUT ідемпотентний, а простий PATCH не завжди? **A:** PUT використовує повне тіло як новий стан, тому десять однакових запитів дають однаковий результат. Простий merge-patch теж ідемпотентний для заміни значень. Проблема виникає з адитивною логікою (інкремент, append) в обробнику. JSON Patch із явними `replace` операціями завжди ідемпотентний. **Q:** У чому різниця між JSON Patch і JSON Merge Patch? **A:** JSON Patch (RFC 6902) - це масив операцій: `add`, `remove`, `replace`, `test`. JSON Merge Patch (RFC 7396) - простіший diff-об'єкт, де `null` означає видалення поля. Merge Patch простіше писати, але не підтримує порядок і атомарний test-and-set. **Q:** Як спроектувати безпечну систему PATCH для конкурентного редагування? **A:** Використовуй ETags і заголовок `If-Match`. Клієнт читає ресурс і отримує ETag, потім надсилає PATCH із `If-Match: "<etag>"`. Якщо хтось інший змінив ресурс, сервер повертає 412. Клієнт знову забирає актуальну версію і повторює запит. Для колаборативного редагування додай JSON Patch `test` операції для атомарної перевірки передумов. **Q:** Коли PUT порушує ідемпотентність? **A:** Коли сервер генерує або змінює поля при кожному записі незалежно від тіла, наприклад `updatedAt`. Тіло однакове, але збережений ресурс відрізняється після кожного виклику. ## Приклади ### Базовий: оновлення профілю користувача ```javascript // PUT - потрібно надіслати ВСІ поля, інакше дані будуть втрачені app.put('/users/:id', async (req, res) => { // replaceOne перезаписує весь документ await User.replaceOne({ _id: req.params.id }, req.body); res.json(await User.findById(req.params.id)); }); // PATCH - надсилаємо тільки зміни app.patch('/users/:id', async (req, res) => { // $set із частковим тілом, незазначені поля залишаються await User.updateOne({ _id: req.params.id }, { $set: req.body }); res.json(await User.findById(req.params.id)); }); ``` PUT використовує `replaceOne`, що перезаписує весь документ. PATCH використовує `updateOne` із `$set`, тому змінюються тільки надіслані поля. Я бачив, як junior-розробники деплоїли PUT-обробники, які стирали номери телефонів тижнями, перш ніж хтось це помічав. ### Середній рівень: JSON Patch із конкурентним захистом ```javascript const jsonPatch = require('fast-json-patch'); app.patch('/users/:id', async (req, res) => { const user = await User.findById(req.params.id); const clientEtag = req.headers['if-match']; if (clientEtag && clientEtag !== user.etag) { return res.status(412).json({ error: 'Precondition Failed' }); } // req.body - масив JSON Patch операцій const patched = jsonPatch.applyPatch(user.toObject(), req.body).newDocument; await User.replaceOne({ _id: req.params.id }, patched); res.json(patched); }); // Клієнт надсилає: // PATCH /users/123 // If-Match: "etag-abc" // Content-Type: application/json-patch+json // [{ "op": "replace", "path": "/email", "value": "new@mail.com" }] ``` Операція `test` додає ще один рівень захисту. `{"op": "test", "path": "/email", "value": "old@mail.com"}` кидає помилку перед `replace`, якщо поточне значення не збігається, що блокує застарілі записи до того як вони відбудуться.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.