Skip to main content

Яка різниця між PUT i PATCH?

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)

Таблиця порівняння

АспектPUTPATCH
Тіло запитуПовне представлення ресурсуТільки зміни
Незазначені поляПерезаписуються (null або дефолт)Зберігаються
ІдемпотентністьТакНе гарантовано
Content-Typeapplication/jsonapplication/json-patch+json або application/merge-patch+json
RFCRFC 7231RFC 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, якщо поточне значення не збігається, що блокує застарілі записи до того як вони відбудуться.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?