What is the difference between PUT and PATCH?
PUT vs PATCH - PUT replaces the entire resource with the body you send; PATCH updates only the fields you specify.
Theory
TL;DR
- PUT = full replacement: missing fields get overwritten to null or a schema default
- PATCH = partial update: only specified fields change, the rest stay untouched
- Analogy: PUT is handing over a brand new passport; PATCH is sticking a new sticker on the existing one
- Idempotent: PUT always yes. PATCH - not guaranteed, depends on implementation
- Rule: client holds full state, small resource → PUT. Large resource, few changes → PATCH
Quick example
// PUT /users/123 - replaces the ENTIRE user object
PUT /users/123
Content-Type: application/json
{
"id": 123,
"name": "Alice",
"email": "alice@new.com",
"phone": null
}
// If original had phone: "123-456", it is now null
// PATCH /users/123 - updates ONLY email, phone stays intact
PATCH /users/123
Content-Type: application/json
{
"email": "alice@new.com"
}
// phone: "123-456" is preservedPUT overwrites unspecified fields. PATCH leaves them alone.
Key difference
PUT treats the request body as the new authoritative state of the resource. Whatever is missing from the body gets overwritten with null or a server-defined default, per RFC 7231. PATCH applies a diff: only named fields change, everything else is preserved. This is defined in RFC 5789. The practical result is that a careless PUT destroys data with no error thrown.
When to use
- Client controls the full resource and it is small → PUT (config overwrite, CRUD form that always sends every field)
- Large resource with many fields, updating just one or two → PATCH (user profiles, billing details)
- Client cannot afford to fetch the full current state first → PATCH (saves bandwidth)
- Need an idempotent full reset → PUT
- Concurrent updates are possible → PATCH with JSON Merge Patch (RFC 7396) or JSON Patch (RFC 6902)
Comparison table
| Aspect | PUT | PATCH |
|---|---|---|
| Body | Complete resource representation | Partial changes only |
| Unspecified fields | Overwritten (null or schema default) | Preserved |
| Idempotent | Yes | Not guaranteed |
| Content-Type | application/json | application/json-patch+json or application/merge-patch+json |
| RFC | RFC 7231 | RFC 5789 |
| When to use | Full state replacement, small resources | Sparse updates, large resources |
How the server handles each
A server receiving PUT replaces the stored resource entirely and saves the new body. Fields absent from the body get set to null or schema defaults. This is why a PUT with only a subset of fields loses the rest without any warning.
For PATCH, the server applies only the named keys. Libraries like fast-json-patch in Node.js handle this through operations: {"op": "replace", "path": "/email", "value": "new@mail.com"} touches only that path in the JSON tree.
Common mistakes
Mistake 1: using PUT for a partial update
// Wrong: PUT omits phone, so server sets phone to null
fetch('/users/1', {
method: 'PUT',
body: JSON.stringify({ name: 'Alice' })
});
// Result: phone is erased. No error, no warning.
// Fix: use PATCH, or fetch the full object first and PUT the merged resultMistake 2: non-idempotent PATCH handler
// Wrong: increments on every call - retrying this doubles the count
app.patch('/items/:id', (req, res) => {
item.count += req.body.amount;
});
// Right: replace the value directly - same result on every retry
app.patch('/items/:id', (req, res) => {
item.count = req.body.count;
});Mistake 3: wrong Content-Type for PATCH
// Wrong: some servers treat application/json as a full PUT
headers: { 'Content-Type': 'application/json' }
// Correct for JSON Patch ops array:
headers: { 'Content-Type': 'application/json-patch+json' }
// Correct for merge patch (object diff, null means delete):
headers: { 'Content-Type': 'application/merge-patch+json' }Mistake 4: PUT without If-Match in concurrent environments
A blind PUT overwrites changes made by another client between your read and your write. Add an ETag and send If-Match: "abc123" with the PUT. The server returns 412 Precondition Failed if the resource changed, forcing a re-fetch.
Real-world usage
- Firebase:
update()= PATCH semantics (preserves fields),set()= PUT semantics - Stripe API:
PATCH /customers/{id}for sparse billing updates - Strapi CMS: uses JSON Patch for partial content updates
- React Query / SWR:
useMutationwith PATCH for optimistic UI - Express.js + MongoDB:
replaceOnefor PUT (full replace),updateOnewith$setfor PATCH
Follow-up questions
Q: Why is PUT idempotent but simple PATCH not always?
A: PUT uses the full body as the new state, so ten identical requests produce the same result. Simple merge-patch is also idempotent when it replaces a value. The problem is additive logic (increment, append) inside the handler. Use JSON Patch with explicit replace operations to guarantee idempotence.
Q: What is the difference between JSON Patch and JSON Merge Patch?
A: JSON Patch (RFC 6902) is an array of operations: add, remove, replace, test. JSON Merge Patch (RFC 7396) is a simpler object diff where null means delete the field. Merge Patch is easier to write but lacks operation ordering and atomic test-and-set support.
Q: How do you design a safe PATCH system for concurrent editing?
A: Use ETags and the If-Match header. The client reads the resource and gets an ETag, then sends PATCH with If-Match: "<etag>". If someone else changed the resource, the server returns 412. The client re-fetches and reapplies. For collaborative editing, add JSON Patch test operations to assert preconditions before each mutation.
Q: When does PUT break idempotence?
A: When the server generates or modifies fields on every write regardless of body, like updatedAt timestamps. The body is identical but the stored resource differs after each call.
Examples
Basic: updating a user profile
// PUT - must include ALL fields or data is lost
app.put('/users/:id', async (req, res) => {
// replaceOne overwrites the entire document
await User.replaceOne({ _id: req.params.id }, req.body);
res.json(await User.findById(req.params.id));
});
// PATCH - send only what changed
app.patch('/users/:id', async (req, res) => {
// $set with partial body; unmentioned fields stay intact
await User.updateOne({ _id: req.params.id }, { $set: req.body });
res.json(await User.findById(req.params.id));
});PUT uses replaceOne, which overwrites the whole document. PATCH uses updateOne with $set, so only the sent fields change. I have seen junior devs ship PUT handlers that quietly erased phone numbers for weeks before anyone noticed.
Intermediate: JSON Patch with concurrent safety
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 is a JSON Patch ops array
const patched = jsonPatch.applyPatch(user.toObject(), req.body).newDocument;
await User.replaceOne({ _id: req.params.id }, patched);
res.json(patched);
});
// Client sends:
// PATCH /users/123
// If-Match: "etag-abc"
// Content-Type: application/json-patch+json
// [{ "op": "replace", "path": "/email", "value": "new@mail.com" }]The test operation adds another safety layer. {"op": "test", "path": "/email", "value": "old@mail.com"} throws before the replace if the current value does not match, blocking stale writes before they happen.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.