Яка різниця між PUT i PATCH?
PUT vs PATCH - PUT повністю замінює ресурс тілом запиту; PATCH оновлює тільки вказані поля.
Теорія
TL;DR
- PUT = повна заміна: поля, яких немає в тілі, перезаписуються null або дефолтом зі схеми
- PATCH = часткове оновлення: змінюються тільки вказані поля, решта залишається
- Аналогія: PUT - це новий паспорт замість старого; PATCH - наклейка поверх чинного
- Ідемпотентність: PUT завжди, PATCH - не гарантовано, залежить від реалізації
- Правило: клієнт тримає повний стан, ресурс невеликий → PUT. Великий ресурс, мало змін → PATCH
Короткий приклад
// 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 для часткового оновлення
// Неправильно: PUT без phone - сервер ставить phone = null
fetch('/users/1', {
method: 'PUT',
body: JSON.stringify({ name: 'Аліса' })
});
// Результат: phone стерто. Без помилки, без попередження.
// Виправлення: використовуй PATCH або спочатку забери повний об'єкт і відправ злитий результатПомилка 2: не ідемпотентний PATCH-обробник
// Неправильно: інкремент при кожному виклику, повторний запит подвоїть лічильник
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
// Неправильно: деякі сервери сприймають 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. Тіло однакове, але збережений ресурс відрізняється після кожного виклику.
Приклади
Базовий: оновлення профілю користувача
// 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 із конкурентним захистом
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, якщо поточне значення не збігається, що блокує застарілі записи до того як вони відбудуться.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.