HTTP methods, safety and idempotency
What GET, POST, PUT, PATCH and DELETE promise, which are safe and which are idempotent, and why PUT with half a body deletes the other half.
After this lesson you can
- State which methods RFC 9110 defines as safe and which as idempotent
- Explain why a second DELETE returning 404 is still idempotent
- Choose between PUT and PATCH and predict what each does to omitted fields
- Apply a JSON Merge Patch by hand
The method is the verb of a request. HTTP semantics (RFC 9110) defines what each one means, and clients, proxies and caches rely on those meanings — so an API that uses a method for something else is not just unconventional, it is lying to the infrastructure in between.
| Method | Meaning | Safe | Idempotent |
|---|---|---|---|
GET | Transfer a representation of the resource | yes | yes |
HEAD | Same as GET, headers only, no body | yes | yes |
OPTIONS | Describe what the resource supports | yes | yes |
PUT | Create or replace the resource's state with the body | no | yes |
DELETE | Remove the resource | no | yes |
POST | Let the resource process the body by its own rules | no | no |
PATCH | Apply a set of changes (RFC 5789) | no | no |
Safe and idempotent are definitions
Safe means the client is asking for no change on the server: a read. The server may still log the request or bump a counter; what matters is that the client did not request a state change and cannot be blamed for one. That is why crawlers and link prefetchers follow GET links freely.
Idempotent means sending the same request N times has the same
intended effect on the server as sending it once. It says nothing
about the response. The first DELETE /orders/7 returns 204 and the
second returns 404, yet the server ends in the same state — order 7 is
gone — so DELETE is idempotent. Every safe method is also idempotent.
Idempotency is what makes a retry safe: if a connection drops before the response arrives, the client cannot know whether the request was applied, and only an idempotent request can simply be sent again.
function makeStore() { const orders = new Map(); let nextId = 100; return { post(body) { const id = nextId++; orders.set(id, { id, ...body }); return 201; }, put(id, body) { const existed = orders.has(id); orders.set(id, { id, ...body }); return existed ? 200 : 201; }, del(id) { return orders.delete(id) ? 204 : 404; }, ids: () => [...orders.keys()].sort((a, b) => a - b), };} export function run() { const s1 = makeStore(); const postStatuses = [s1.post({ item: "pen" }), s1.post({ item: "pen" })]; const s2 = makeStore(); const putStatuses = [s2.put(7, { item: "pen" }), s2.put(7, { item: "pen" })]; const s3 = makeStore(); s3.put(7, { item: "pen" }); const deleteStatuses = [s3.del(7), s3.del(7)]; return { post: { statuses: postStatuses, ids: s1.ids() }, put: { statuses: putStatuses, ids: s2.ids() }, delete: { statuses: deleteStatuses, ids: s3.ids() }, };}PUT replaces, PATCH changes
PUT sends the whole new state of the resource. Fields the client
leaves out are not "left alone"; they are absent from the new state.
That is exactly why PUT is idempotent — the result does not depend on
what was there before — and exactly why a client that PUTs
{"email": "..."} to update an email address wipes everything else.
PATCH sends a description of changes, and its format is defined by the media type. The two standard JSON ones:
- JSON Merge Patch (RFC 7396,
application/merge-patch+json): a partial document. Present fields are set, objects merge recursively,nullremoves a field, and arrays are replaced wholesale. - JSON Patch (RFC 6902,
application/json-patch+json): a list of operations such asadd,remove,replace,move,copy,test.
const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v); function mergePatch(target, patch) { if (!isObj(patch)) return patch; const out = isObj(target) ? { ...target } : {}; for (const [k, v] of Object.entries(patch)) { if (v === null) delete out[k]; else out[k] = mergePatch(out[k], v); } return out;} // Only the one JSON Patch operation this example needs.function jsonPatchAppend(doc, field, value) { return { ...doc, [field]: [...doc[field], value] };} export function run() { const user = { id: 7, name: "Nino", email: "nino@old.example", phone: "555-0100", tags: ["vip"] }; const body = { email: "nino@new.example" }; const afterPut = { id: 7, ...body }; const afterMerge = mergePatch(user, body); const removePhone = mergePatch(user, { phone: null }); const mergeTwice = mergePatch(mergePatch(user, body), body); const appendTwice = jsonPatchAppend(jsonPatchAppend(user, "tags", "beta"), "tags", "beta"); return { afterPut, afterMerge, removePhone, mergeTwiceSameAsOnce: JSON.stringify(mergeTwice) === JSON.stringify(afterMerge), tagsAfterAppendTwice: appendTwice.tags, };}A merge patch, applied twice, lands on the same document. A JSON Patch
add to /tags/- (the end of the array) appends again every time. PATCH
is not idempotent in general, and a client must not retry one blindly.
POST
POST means "process this according to your own rules". Its most common
use is creating a member of a collection when the server chooses the
id — POST /orders answers 201 Created with a Location header. If
the client chooses the id, PUT /orders/{id} can create it instead, and
then the create is idempotent for free.
Common mistakes
- A GET that changes state (
GET /orders/7/approve). Prefetchers, crawlers and caches will trigger it without anyone clicking. - Using PUT for partial updates: a client sends only the changed field and silently erases the rest.
- Assuming PATCH is idempotent: an append-style operation runs twice on a retry, as the example shows.
- Believing idempotent means "same response every time" — it is about server state, not the status code you get back.
What interviewers ask
- Is DELETE idempotent if the second call returns 404? — yes; idempotency is about the effect on server state, not the response.
- PUT or PATCH for updating a user's email? — PATCH with only the changed field; PUT must carry the full representation and replaces it.
- Why can a client retry a PUT after a timeout but not a POST? — PUT is idempotent so repeating it cannot change the outcome; a repeated POST may create a second resource unless the API offers idempotency keys.
Try it yourself
2 visible tests · 4 hidden testsImplement mergePatch(target, patch) following RFC 7396 and return the
result without mutating either argument:
- If
patchis not a plain object (a string, number, boolean,nullor an array), the result ispatchitself. - Otherwise start from a copy of
targetiftargetis a plain object, or from{}if it is not. For every key inpatch: anullvalue removes that key from the result; any other value is merged recursively,result[key] = mergePatch(result[key], value).
Arrays are never merged element by element; they replace. Key order in the returned object does not matter.
mergePatch({"city":"Tbilisi","name":"Nino","email":"old@example.com"}, {"email":"new@example.com"})mergePatch({"name":"Nino","phone":"555-0100"}, {"phone":null})
Sign up to run the tests, check the hidden ones and save your progress. Sign up