System Design Concepts · API Design
HTTP methods: what each verb promises
A method's promise about safety and idempotency decides when a retry is safe, when a cache is legal, and PUT versus POST.
Verbs have a meaning and a code
- The first row is what each of the five verbs means. GET reads, POST creates, PUT replaces the whole resource, PATCH changes only the fields sent, and DELETE removes.
- Row two is safe: does the call change anything on the server? Only GET is safe, because a GET only reads. The other four all write, so none of them is safe.
- Row three is idempotent: does sending the same call again change anything more? GET, PUT and DELETE are idempotent, POST is not, and PATCH depends on what the patch does.
- Row four shows who sets the id. The server picks the id for POST, but the client sets the id for PUT, PATCH and DELETE. GET reads an id the client asks for.
- Row five is the success code, the status the server sends back when the call works. The codes are 200 for GET, 201 for POST, 200 or 204 for PUT, 200 for PATCH, and 204 for DELETE.
- The last step reads the whole column for one verb, POST: not safe and not idempotent, the server picks the id, and 201 on success. So the verb at the top of the column and the code at the bottom are one promise.
PUT replaces, PATCH merges
- The server stores one document at /users/123. The document has three fields, each with a value: name Ada, email ada@example.com, plan free. A client is on the other end of the network connection. Both PUT and PATCH can change these three fields.
- A PUT body with all three fields arrives over the wire. PUT replaces every stored row with the body's values. So the record is rewritten, but it still shows the same values: name Ada, email ada@example.com, plan free.
- A PUT body arrives with only name and plan. PUT replaces the whole document, so the server clears all three rows, writes back only those two, and leaves the stored record with no email field.
- The record again holds all three fields, and then the server receives a PATCH body with only the plan field. The plan field changes from free to pro. Name and email are not changed.
- The server receives the full PUT twice, then the same PATCH twice. The record holds name Ada, email ada@example.com, plan pro after every request. Setting plan to pro twice leaves pro, so PUT and PATCH are both idempotent.
- The record now has a fourth field, tags, which starts as an empty list. The same PATCH with op add on /tags/- sent twice. The first request writes vip. The second request appends another vip, because op add appends to the list. So this PATCH is not idempotent.
- The rule has two parts. PUT sends the whole record, so use PUT when the client has every field. PATCH sends only the change, so use PATCH when the client does not have every field. On one route, each method should mean one thing.
POST keeps adding, PUT stays
- A client and a server are joined by one wire, the network link between them. The server's tally, its count so far, is 0.
- The client sends a POST /users request to the server. The server picks an id, 41, so the server's count rises to 1.
- The server sends its reply back over the network, but the reply is lost halfway. The client gets no reply, so the client cannot tell whether the request never arrived, or arrived and only the answer was lost.
- The client has no reply, so the client sends the same POST request again. The server creates a second user, id 42, so the count rises to 2.
- The client retries once more. The server creates user 43, so the count rises to 3. Three users now exist where one was asked for.
- The request becomes PUT /users/123. The client picks the address itself, so the server's list has only one slot for that user.
- The reply is lost again, so the client retries twice more. Each retry rewrites the same user, /users/123, so the count stays at 1.
- A charge cannot be a PUT, so the request becomes POST /charges with an Idempotency-Key, 9f2. The reply is lost and the client retries twice with the same key, and the count stay at 1.
© LearnThatStack - diagrams may not be republished without permission.
Every HTTP verb is a promise to the client about what a call does. A broken promise means a retry duplicates a charge, or a cache stores an answer that should never be cached. Each verb has a plain meaning: GET reads and POST creates. PUT replaces the whole resource, PATCH changes only the fields in the request, and DELETE removes.
Row two asks whether the call changes anything on the server. Only GET is safe, because GET changes nothing. The other four methods can write. A safe method is one you can call without changing anything, so a browser may prefetch a GET but not a POST.
The second question, in row three, is whether the method is idempotent. Idempotent means a repeated call leaves the server in the same state. GET, PUT and DELETE are idempotent, but POST is not. PATCH depends on what the patch body does, so no one can promise in advance that a PATCH is idempotent.
Idempotent is about the stored state on the server, not about the reply. Send DELETE for the same user twice. The server answers 204 the first time and 404 the second. The two replies differ. But the user is gone either way, so DELETE is idempotent.
The fourth question is who sets the id. The server picks the id for POST, so the client cannot know the address before sending the request. The client sets the target for PUT, PATCH and DELETE, and GET reads an id the client already knows. The client already knows the target, so the client can repeat a PUT without checking whether the first one worked.
Row five is the expected success code, the other half of the contract. GET answers 200 and POST answers 201, because something new now exists. PUT answers 200 or 204, PATCH answers 200, and DELETE answers 204. A 204 means the work is done and there is nothing to send back.
Read the POST column top to bottom: it's not safe, not idempotent, and server picks an id. The bottom of the column is the status code 201. POST is the only verb with every guarantee switched off, so you have to protect POST yourself.
We called PUT a replace and PATCH a change. That difference can make people lose data. The server holds one document at /users/123 with three fields filled in: name Ada, email ada@example.com, plan free. A client at the other end of the connection can write to those fields with either PUT or PATCH.
PUT replaces the record instead of editing the record. A PUT body with all three fields arrives over the network, so PUT clears every stored field, then writes all three fields again from the body. The record now reads name Ada, email ada@example.com, plan free, exactly as before. Here the replacement happens to match what was stored before.
Now a PUT body arrives with only name and plan. PUT replaces the whole stored record again, so the server clears all three fields and fills only two. Nothing arrives for email, so the record no longer has an email field. A field left out of a PUT body is a deletion, not a request to keep the old value.
The stored record again has all three fields filled: name, email and plan. The PATCH body carries only the plan field. The server changes plan from free to pro, but name and email stay as they were. PATCH sends the change, not the whole record, so the server merges that change into what is already stored.
The server receives the full PUT twice, then the same PATCH twice. The record holds the same values after each call: name Ada, email ada@example.com, plan pro. Setting plan to pro twice still leaves the plan at pro. Both calls are idempotent here, so a client that never got the reply can safely send either call again.
A fourth field, tags, starts as an empty list. PATCH comes in two common forms: JSON Merge Patch sends the fields to merge, and JSON Patch sends a list of operations. One add operation on /tags/- runs twice, so the first run writes vip and the second run appends a second vip. Append is not idempotent, because each run adds one more entry.
Use PUT when the client has every field, because PUT sends the whole record. Use PATCH when the client has only the change. Give each method on a route one meaning, so PUT and PATCH never do the same job. A PUT to an id that does not exist may create the record and return 201.
The server keeps a count of users, and right now that count is 0. That count will change here, and how it changes depends on the operation the client sends.
A POST request to /users reaches the server. The server picks the id itself, in this case 41, and the user count becomes 1. One request created one user, as expected. Now the server sends the reply back to the client.
The reply is lost halfway back across the network, so the client receives nothing. Now the client has two possible explanations. Either the reply was lost on the way back, or the request itself failed. In both cases the client receives nothing, so the client cannot tell them apart.
The client does not know what happened, so it sends the same request again. The server handles this second request and creates user 42, so the count of users rises to 2. The server treated the two as separate requests, because nothing in the second request marked it as a repeat. Both requests were valid POSTs.
One more retry creates user 43. The server now holds three users, and the caller wanted only one. Replace users with charges, and the same retries charge a customer three times. POST makes no promise against duplicates, so POST is not idempotent.
The client sends a PUT request to /users/123. The client chose that address itself, so the server only acts on the one record at that address. The network is no better than before. But the address now names one record instead of a collection, and that change alone makes the retry safe.
The reply is lost again, so the client retries two more times. Each retry writes the same record with the same body. The count stays at 1. The server did the work three times, but the state after three calls is the same as the state after one. Repeating the call changed nothing, so the call is idempotent.
A charge cannot be a PUT, because the client has no id to set. So the client sends POST /charges with an Idempotency-Key header, 9f2, and retries twice under the same key when the reply is lost. The server stored its first response under that key, so every repeat gets that same response and the charge count stays at 1. Stripe uses this mechanism and keeps each key for 24 hours.
A method is a promise about two things. Does the call change state, and does calling again change anything more? That promise decides when a retry is safe, when a cache may keep the answer, and PUT versus POST. Pick the verb whose promise matches the work, and protect POST with a key that detects a repeat.
In interviews · 3 questions
Related Questions
- 01
- 02
- 03