API Design
HTTP Methods, Status Codes and Idempotency in Practice
GET, POST, PUT, PATCH and DELETE used correctly, the status codes that matter, and why idempotency decides whether a retry is safe.
On this page
- Words used in this lesson
- The five verbs, in one table
- Lab: what a retry really does
- GET: the one that must be boring
- POST: create, or "do this thing"
- PUT: replace the whole thing
- PATCH: change a few fields
- DELETE
- HEAD and OPTIONS
- Status codes: the short list that covers almost everything
- Optimistic concurrency with ETag and If-Match
- Errors and retries: what should a client do?
- 202 Accepted: when the work takes time
- Choosing: a short decision list
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: HTTP gives you five main verbs and a small set of status codes. Use them as they were meant, and clients, caches and retry tools just work. The one idea to take away is idempotency: if the network fails and the client sends the same request again, will the world be the same as if it had been sent once?
Imagine a user taps "Pay" on a phone in a lift. The request leaves the phone. The server charges the card. The reply is lost because the signal drops. The phone shows an error. The user taps "Pay" again. Was the card charged once or twice?
That one question explains most of this lesson. In a network, you can never be sure whether a request that got no answer was processed. So clients retry. And a retry is only safe if the API was designed for it. The HTTP methods carry rules for this, and good API design means following them.
This is lesson 1 of ten. The previous lesson, What Makes a Good API, set up resources and URLs. Here we cover the verbs and the numbers. The next one, Request and Response Design, covers the JSON shapes and error format. The full retry solution, idempotency keys, comes in Lesson 5. For a shorter interview-style view of the same facts, see Browser Lesson 6 (HTTP semantics).
What I ran / checked. All code ran on Linux with Node.js v22.22.0, using only node:http and curl, against one small "orders" server (on port 4200) that I wrote for this lesson. Output is real; I removed date, connection and content-length headers to keep it short. Sources I read (in my own words): the Zalando RESTful API Guidelines chapter on HTTP requests (the section that defines safe, idempotent and cacheable, the table of methods, and the rules for PUT, POST, PATCH, DELETE and idempotent POST), the Zalando chapter on HTTP status codes and errors (the 429, Retry-After and problem JSON rules), and the Microsoft Azure REST API Guidelines (the return-code table, the idempotency section, 403 vs 404, and the conditional request section). Could not reach: the RFC Editor and IETF sites were blocked, so I did not read RFC 9110 myself. The method table and definitions below match how the Zalando guidelines quote RFC 9110 section 9.2, and the status code meanings are from my own knowledge of HTTP. Please check RFC 9110 before you quote an exact sentence in a review. Retry-After and Allow behaviour was tested by me only on my own toy server.
Words used in this lesson
- Safe method is a method that is meant only to read, and must not change server state in a way the client asked for. GET is safe.
- Idempotent (say "eye-dem-POH-tent") means doing the same request once or many times leaves the server in the same state. The response may be different. DELETE is idempotent.
- Retry is when a client sends the same request again because it got no answer, or an answer that says "try later".
- Cacheable means a response may be stored and reused by a browser or a CDN.
- Payload / body is the data sent in a request or response.
- Header is a name-and-value line sent before the body, such as
Content-Type: application/json. - ETag is a short label for a version of a resource. It changes when the resource changes.
- Conditional request is a request with a condition, such as
If-Match: "abc", which says "only do this if the version is stillabc". - Optimistic concurrency means we let people edit freely, and reject a save if someone else changed the thing in the meantime.
- 4xx codes mean the client did something wrong. 5xx codes mean the server failed.
- Content negotiation is the client and server agreeing on a format using headers such as
AcceptandContent-Type.
The five verbs, in one table
Here are the methods you will use 99% of the time. Safe and idempotent are the two properties that matter for retries and caches. The values follow the table in the Zalando guidelines, which cites the HTTP specification (RFC 9110, section 9.2):
| Method | Used to | Safe | Idempotent | Cacheable |
|---|---|---|---|---|
| GET | read | yes | yes | yes |
| HEAD | read headers only | yes | yes | yes |
| POST | create, or run an action | no | no (but can be designed to be) | rarely |
| PUT | replace a whole resource | no | yes | no |
| PATCH | change part of a resource | no | no (but can be designed to be) | no |
| DELETE | remove | no | yes | no |
| OPTIONS | ask what is allowed | yes | yes | no |
Learn the middle two columns by heart. They are asked in almost every API interview.
What "safe" and "idempotent" mean exactly
- Safe means read-only in intent. A safe request must not cause a change that the client asked for. The server may still write a log line or update a counter. Those are not "intended effects".
- Idempotent means the same intended effect on the server's state whether it ran once or many times. The Zalando text adds an important note: it does not require the same response. The first
DELETE /orders/10can return204, and the second can return404. Both are idempotent, because after either one the order is gone.
Every safe method is idempotent. Not every idempotent method is safe (PUT and DELETE change things, but repeating them changes nothing more).
Lab: what a retry really does
Let us prove it with a server. It keeps orders in memory. I added one test switch: it can run a POST fully and then cut the connection before answering. This simulates a lost response. (This switch is only for the lab.)
First the normal case:
$ curl -i -H content-type:application/json -X POST localhost:4200/orders -d '{"item":"pen","qty":2}'HTTP/1.1 201 Createdcontent-type: application/jsonlocation: /orders/1{"id":1,"item":"pen","qty":2,"status":"new"}Now the nasty case. I turn on the switch, then send POST for a book. The server creates the order and then drops the connection:
$ curl -sS -m 3 -H content-type:application/json -X POST localhost:4200/orders -d '{"item":"book","qty":1}'curl: (52) Empty reply from serverThe client has no idea what happened. A sensible client retries. Let us do that:
$ curl -sS -H content-type:application/json -X POST localhost:4200/orders -d '{"item":"book","qty":1}' -o /dev/null -w "retry status %{http_code}\n"retry status 201$ curl localhost:4200/orders{"items":[{"id":1,"item":"pen","qty":2,"status":"new"},{"id":2,"item":"book","qty":1,"status":"new"},{"id":3,"item":"book","qty":1,"status":"new"}]}Look at ids 2 and 3. The customer has two books. POST is not idempotent: every call creates a new order. The retry did exactly what HTTP says POST does, so the bug is in the design, not in curl.
Now compare PUT. With PUT the client chooses the id, so sending the same request twice means "make sure order 10 looks like this":
$ curl -i -H content-type:application/json -X PUT localhost:4200/orders/10 -d '{"item":"lamp","qty":1}'HTTP/1.1 201 Createdetag: "09c8c867"{"id":10,"item":"lamp","qty":1,"status":"new"}$ curl -i -H content-type:application/json -X PUT localhost:4200/orders/10 -d '{"item":"lamp","qty":1}'HTTP/1.1 200 OKetag: "09c8c867"{"id":10,"item":"lamp","qty":1,"status":"new"}Same state both times (see: the same ETag "09c8c867"). The first call created the order (201), the second replaced it with identical data (200). A lost response is no problem: retry as often as you like.
And DELETE twice:
$ curl -i -X DELETE localhost:4200/orders/10HTTP/1.1 204 No Content$ curl -i -X DELETE localhost:4200/orders/10HTTP/1.1 404 Not Found{"error":"order not found"}The response changed (204 then 404) but the state is the same: no order 10. That is idempotent. Some teams prefer to return 204 even the second time, so a retrying client never sees an "error" for a delete that already worked. The Microsoft guidelines say exactly that: return 204 for DELETE even if the thing does not exist. Zalando lets you choose. Whichever you pick, document it, and use it everywhere.
Idempotent does not mean "returns the same response". A very common wrong answer in interviews. It is about the effect on the server's state. Also, a DELETE that removes "the oldest order" each time is not idempotent, because the second call deletes a different order. The rule only holds when the request names a specific target.
So how do I make POST safe to retry?
There are three common ways. Zalando's guidelines list them as a "conditional key", a "secondary key" and an "idempotency key":
- Let the client choose the id and use PUT. Instead of
POST /orders, the client generates a random id (a UUID) and callsPUT /orders/{uuid}. A retry just replaces the same order. This is the simplest fix when it fits. - A secondary unique key. The server rejects a second order that has the same natural key (for example the same cart id). The second call returns the first order or a conflict.
- An idempotency key header. The client sends
Idempotency-Key: <random value>onPOST. The server remembers the key and the response for some time. A repeat with the same key returns the same response and does not run the action again. Payment APIs use this a lot. Lesson 5 builds it and runs it.
For now, remember the rule: every API that can be retried by a client needs a plan for POST. If you cannot say what happens on a double POST, you are not done.
GET: the one that must be boring
GET reads a resource. It must be safe. Four rules:
- No side effects the client asked for. Not "delete", not "send email", not "mark as read" if that matters.
- Do not use a body. Some tools drop or reject a body on GET. For complex searches, the Zalando guidelines allow a documented
GETwith a body as an exception, and also describe other ways. Prefer query parameters, orPOST /orders/searchfor large queries. - Return
200with the resource, or404if it does not exist. Return304 Not Modifiedwhen the client sent a matchingIf-None-MatchETag (see below). - Be careful with secrets in the URL. URLs end up in logs, browser history and proxies. Put tokens in a header, not in
?token=....
POST: create, or "do this thing"
POST has two jobs:
- Create a resource in a collection:
POST /orders->201 Createdwith aLocationheader with the new address, and the new resource in the body. Microsoft's table says the same: POST that creates returns201with the URL of the created resource. - Run an action that is not a plain create or update:
POST /orders/9001:cancel->200(or202, see below).
POST is the "everything else" method, which is why it is not idempotent by default. If a client has no way to ask for an operation twice safely, use the techniques above.
PUT: replace the whole thing
PUT /orders/10 means "make resource 10 exactly this". Three things follow:
- The body is the full representation. Fields you leave out are removed or reset. This surprises people. Let me show it:
$ curl localhost:4200/orders/1{"id":1,"item":"pen","qty":2,"status":"new"}$ curl -i -H content-type:application/json -X PUT localhost:4200/orders/1 -d '{"item":"pen"}'HTTP/1.1 200 OK{"id":1,"item":"pen","status":"new"}I sent only item, and qty vanished. That is correct PUT behaviour: "replace". If the client wanted to change one field, it should have used PATCH. (In real code, the server should also reject a PUT that is missing a required field, with 400 or 422, which lesson 2 covers. My toy server did not, to show you the danger.)
- PUT is idempotent, as we saw.
- PUT may create. If the resource does not exist, PUT may create it and return
201. Or you may decide it returns404. Both are valid, but choose one and document it. Zalando notes that PUT is usually robust against a missing resource, and recommends it when the client controls the id.
On success PUT returns 200 with the updated resource, or 204 with no body.
PATCH: change a few fields
PATCH /orders/1 with {"status":"paid"} changes only status.
$ curl -i -H content-type:application/json -X PATCH localhost:4200/orders/1 -d '{"status":"paid"}'HTTP/1.1 200 OKetag: "aa605bc1"{"id":1,"item":"pen","qty":2,"status":"paid"}Notes:
- PATCH is not idempotent by definition, because a patch format could say "add 1 to qty". A patch that only sets fields (like ours, which is called a JSON merge patch) is idempotent in practice. Microsoft's guide uses JSON Merge Patch for PATCH. The other format, JSON Patch (RFC 6902), lists operations like
{"op":"add",...}and can be non-idempotent. - In a merge patch,
nullusually means "remove this field". So{"nickname": null}clears it, and leavingnicknameout means "do not touch". Document this, because it is easy to get wrong. - Return the updated resource (
200) so the client does not need another GET.
DELETE
DELETE /orders/10 removes it. Return 204 No Content on success. As shown, a second DELETE can return 204 or 404. Do not use a body on DELETE if you can avoid it, because many tools drop it.
For big or risky deletes, consider soft delete (a deleted_at field, with the item hidden from lists) so you can undo a mistake. That is a product decision, but the API should say which one it is.
HEAD and OPTIONS
- HEAD is GET without the body. Use it to check "does this exist and what is its ETag" cheaply. Most frameworks give it to you for free.
- OPTIONS says what a URL supports. Browsers send it automatically before some cross-site requests (the CORS preflight in Browser Lesson 4). Your
405responses should carry anAllowheader, as ours did:
$ curl -i -X DELETE localhost:4200/ordersHTTP/1.1 405 Method Not Allowedallow: GET, POST{"error":"method not allowed"}Status codes: the short list that covers almost everything
HTTP has dozens of status codes. A good API uses about fifteen. Both the Zalando and Microsoft guidelines say to use only official codes, with their official meanings. Keep it simple:
2xx: it worked
| Code | Name | Use when |
|---|---|---|
| 200 | OK | Success, and there is a body (GET, PUT, PATCH, action) |
| 201 | Created | A new resource now exists. Send Location |
| 202 | Accepted | I took the request, but the work is not done yet (long jobs, lesson 5) |
| 204 | No Content | Success and there is nothing to send (DELETE, some PUTs) |
3xx: look elsewhere or use your copy
| Code | Use when |
|---|---|
| 304 Not Modified | Conditional GET: your cached copy is still good |
| 301/308 | The URL moved forever |
| 302/307 | The URL moved for now |
(In APIs, redirects are rare. If you move a URL, the browser lesson table on 301, 302, 307 and 308 applies.)
4xx: the client's fault
| Code | Name | Use when |
|---|---|---|
| 400 | Bad Request | The request is broken: bad JSON, wrong type, missing field |
| 401 | Unauthorized | Not logged in (no or bad credentials). The name is misleading |
| 403 | Forbidden | Logged in, but not allowed |
| 404 | Not Found | No such resource (or you hide it) |
| 405 | Method Not Allowed | Wrong method for this URL. Send Allow |
| 409 | Conflict | The request is fine, but conflicts with the current state |
| 412 | Precondition Failed | An If-Match or similar condition failed |
| 415 | Unsupported Media Type | You sent a body format I do not read |
| 422 | Unprocessable Content | The JSON is valid, but the values break the rules |
| 429 | Too Many Requests | Rate limit. Send Retry-After (lesson 5) |
5xx: the server's fault
| Code | Use when |
|---|---|
| 500 | Something unexpected broke. A bug |
| 502/504 | A server behind me failed or was too slow |
| 503 | I am overloaded or in maintenance. Send Retry-After if you can |
Two things about this list. The 4xx codes tell clients whether to retry: a 400, 404 or 422 will fail again the same way, so do not retry. A 503 or 429 may work later, so retry with a wait. Zalando's text adds that clients should be careful when retrying a 500 or 502, because the cause is unknown and may continue.
Let us see the interesting ones on our server.
401 vs 403, and why 404 is sometimes the polite lie
401: "I do not know who you are." Send credentials.403: "I know who you are, and the answer is no."
Microsoft's guidelines add a subtle point. Normally return 403 when the user may not see the resource. But if saying 403 would reveal that the thing exists (a private document, someone else's account), return 404 so that nothing leaks. Choose by asking: does the existence of this thing count as a secret?
400 vs 422
400is for a request the server cannot even read: broken JSON, a wrong type, a missing required header.422is for a request that is well-formed but semantically wrong: valid JSON, butqtyis-5.
Many APIs use 400 for both. Others use 422 for the second. Both are acceptable. Consistency is the rule. Our server shows the first two cases:
$ curl -i -X POST localhost:4200/orders -d 'item=pen'HTTP/1.1 415 Unsupported Media Type{"error":"send application/json"}$ curl -i -H content-type:application/json -X POST localhost:4200/orders -d '{bad'HTTP/1.1 400 Bad Request{"error":"body is not valid JSON"}(The first one failed because curl, when you give it -d and no content type, sends application/x-www-form-urlencoded. The server did not read that type and said so with 415. That is a real gotcha when you test by hand.)
409 vs 412: two kinds of "no, because of the current state"
409 Conflict means the request clashes with the state of the resource. Example: our rule says a cancelled order cannot be shipped.
$ curl -i -H content-type:application/json -X PATCH localhost:4200/orders/1 -d '{"status":"cancelled"}'HTTP/1.1 200 OK...$ curl -i -H content-type:application/json -X PATCH localhost:4200/orders/1 -d '{"status":"shipped"}'HTTP/1.1 409 Conflict{"error":"a cancelled order cannot be shipped"}Other examples of 409: creating a user with an email that already exists, or deleting a folder that is not empty.
412 Precondition Failed is for conditional requests. It solves the "lost update" problem, in which two people edit the same thing and the second save silently wipes the first.
Optimistic concurrency with ETag and If-Match
Picture two admins who open the same order. Admin A changes qty to 5 and saves. Admin B, who still sees the old page, changes status and saves the whole object. Admin A's change is gone. To stop this, the server sends an ETag with every GET:
$ curl -i localhost:4200/orders/1HTTP/1.1 200 OKetag: "8a344599"{"id":1,"item":"pen","status":"new"}When the client saves, it sends that tag back in If-Match. The server checks that the resource still has this tag. If yes, it saves. If not, it answers 412.
$ curl -i -H content-type:application/json -X PATCH localhost:4200/orders/1 -H 'If-Match: "8a344599"' -d '{"qty":5}'HTTP/1.1 200 OKetag: "5a49b020"{"id":1,"item":"pen","qty":5,"status":"new"}$ curl -i -H content-type:application/json -X PATCH localhost:4200/orders/1 -H 'If-Match: "8a344599"' -d '{"qty":9}'HTTP/1.1 412 Precondition Failed{"error":"order changed since you read it"}The second client was working from an old copy, so the server refused. The client can now fetch the new version, show the person what changed, and ask again. This is called optimistic concurrency (no locks; we only check at save time). The Microsoft guidelines say to support If-Match, If-None-Match and the date-based versions, and to return the ETag and Last-Modified headers.
The same ETag helps caching: the client sends If-None-Match: "8a344599" on GET, and if nothing changed the server answers 304 Not Modified with no body, saving bandwidth. Browser Lesson 5 (HTTP caching) covers that side.
Some servers return 428 Precondition Required when a write has no If-Match at all, to force every client to use it. That is a stricter design. In my toy server If-Match is optional, so a client that forgets it can still overwrite. Whether that is acceptable is a product decision.
Errors and retries: what should a client do?
A short table you can put in your client library, and in your interview answer:
| Response | Retry? | Why |
|---|---|---|
| network error, timeout | yes, if the method is idempotent (or has an idempotency key) | you do not know if it ran |
| 400, 401, 403, 404, 409, 412, 415, 422 | no | the same request will fail the same way (401: refresh the token first, then retry) |
| 429 | yes, after waiting Retry-After | the server asked you to slow down |
| 500, 502 | carefully: idempotent requests only, few tries | the cause is unknown and may last |
| 503, 504 | yes, with backoff | usually temporary |
Use exponential backoff with jitter: wait 1 s, then 2 s, then 4 s, each with a bit of random time added, and give up after a few tries. Without jitter, a thousand clients that failed together will all retry together and hit the server in one big wave.
The Zalando guideline for rate limits says to use 429 with a Retry-After header that can hold either a number of seconds or a date. The spec says clients should respect it.
202 Accepted: when the work takes time
If an action takes longer than a normal request (a report, a video, a big export), do not hold the connection open. Answer 202 Accepted with a Location that points to a status resource, and let the client poll it:
POST /reports -> 202 Accepted, Location: /reports/77/statusGET /reports/77/status -> 200 {"state":"running"}GET /reports/77/status -> 200 {"state":"done","result":"/reports/77"}Microsoft has a long section on this pattern (they call it a long-running operation with a status monitor). We build one in lesson 5.
Choosing: a short decision list
When you add an endpoint, ask in this order:
- Does it only read? Use
GET. Make it safe. Support ETag if the resource is cacheable. - Does the client know the id? Use
PUTto create or replace. It is idempotent for free. - Is it a partial change? Use
PATCHwith merge patch. AddIf-Matchif two people can edit. - Is it a new thing, and the server chooses the id? Use
POST, return201andLocation. Decide how a retry is handled. - Is it a real action?
POSTon an action URL. Decide if it is idempotent and say so. - Is it a remove?
DELETE. Decide204/404on repeat and keep it consistent.
Try it
- A client sends
PUT /users/7with{"name":"Ada"}three times because of timeouts. What is the state after the third call? Is that the same as after the first? - A client sends
POST /paymentswith{"amount":500}twice. What is the problem? Name two ways to fix it. - For each, give the best status code: (a) a user asks for an invoice that does not exist, (b) a logged-in user asks for another company's invoice, (c) a request has valid JSON but
amountis negative, (d) you accepted an export job that will take 2 minutes, (e) two people saved the same document and the second used an old version tag. - Run the order server's lab: start it, create an order, then send
PATCHwith anIf-Matchheader that is a wrong value. What status do you get? - Change the DELETE handler so that a second DELETE also returns
204. Which is better for a retrying client, and why?
Answers
- The state is the same as after the first call: user 7 has the name "Ada". PUT is idempotent. The response codes could differ (
201first,200after). - Two payments of 500 may be created (POST is not idempotent), so the customer is charged twice. Fixes: an
Idempotency-Keyheader that the server stores with the result, or let the client choose the payment id and usePUT /payments/{uuid}, or a unique secondary key (like the order id), so the second one is rejected or returns the first. - (a)
404. (b)403, or404if the invoice's existence is private. (c)422(or400, if that is your consistent choice). (d)202withLocationof a status resource. (e)412(precondition failed). 412 Precondition Failed. The tag does not match the current one. Try also an old tag, after another change.- Returning
204the second time: the client that retried after a lost response sees success and does not have to treat404as a failure. It also matches the Microsoft guideline. The cost is that you cannot tell the client that it deleted something that never existed (for example a typo in the id).
Interview questions
What is the difference between safe and idempotent? (Must know)
Safe means the request only reads and does not change state in an intended way. Idempotent means that running it many times has the same effect on server state as running it once. All safe methods are idempotent, but PUT and DELETE are idempotent and not safe. For example, DELETE /orders/10 twice leaves the same state as once, so it is idempotent, but it is clearly not safe.
Is DELETE idempotent if the second call returns 404? (Trap)
Yes. Idempotency is about the effect on state, not about the response. After the first DELETE the order is gone, and after the second it is still gone. The response may differ: 204 then 404. Some APIs return 204 both times so that a retrying client sees success. Either is fine if it is documented.
PUT or POST to create a resource? (Must know)
If the server chooses the id, I use POST /collection and return 201 with Location. If the client knows or can generate the id (for example a UUID), I prefer PUT /collection/{id}, because PUT is idempotent and retries are safe. The cost is that the client must be trusted to produce good ids, or the server must validate them.
PUT vs PATCH? (Must know)
PUT replaces the whole resource, so missing fields are dropped or reset, and it is idempotent. PATCH changes only the fields in the body. A merge patch that only sets values behaves idempotently, but PATCH is not guaranteed to be idempotent because a patch can describe relative changes like "add one". I use PATCH for small updates and PUT when the client holds the whole object.
A client times out on POST /payments. What should it do, and how should the API be designed to help? (Senior signal)
The client does not know whether the payment happened, so it needs to retry safely. The API should support an Idempotency-Key header: the client sends a random key with the first attempt and the same key on the retry. The server stores the key and the first response for some time, and on a repeat returns the stored response without charging again. If the same key comes with a different body, the server should reject it with an error. Without this, a retry can charge twice.
When do you return 401 and when 403? (Must know)
401 means the client is not authenticated: there are no credentials, or they are invalid or expired. It should come with a WWW-Authenticate header. 403 means the client is authenticated but not allowed. I add that when the existence of the resource is itself private, I may return 404 instead of 403 to avoid leaking that it exists.
400 or 422 for a validation error? (Trap)
Both are used. 400 is "I could not understand the request" (broken JSON, wrong types). 422 is "I understood it, but the values break the rules". Many teams use 400 for both for simplicity. The important point is to be consistent across the API and to put the details in the body in one error format (lesson 2). A wrong answer is to say one of them is the only correct one.
What is 409 Conflict, and how is it different from 412? (Senior signal)
409 says the request conflicts with the current state of the resource, for example a duplicate unique email or a forbidden state change like shipping a cancelled order. 412 says a condition the client attached to the request (such as If-Match: "tag") was not true. It is used for optimistic concurrency: the client edited an old version. 409 comes from business rules, 412 from the request's own preconditions.
How do ETag and If-Match prevent lost updates? (Senior signal)
The server returns an ETag, a label for the current version, with each GET. The client sends it back in If-Match when saving. If the resource has changed meanwhile, the tags differ and the server returns 412 instead of overwriting. The client then re-reads, merges and tries again. It needs no locks, and it works well when conflicts are rare. For caching, the same ETag with If-None-Match lets the server answer 304.
Why should GET never change data? (Trap)
Many parts of the web assume GET is safe: browsers prefetch it, crawlers follow links, caches store it, retry code repeats it. If a GET deletes or changes data, a crawler or a prefetch can cause real damage, and a cache may even answer without your code running. Also, GET URLs end up in logs. So changes use POST, PUT, PATCH or DELETE.
What do you return from a POST that creates something? (Must know)
201 Created, a Location header with the URL of the new resource, and usually the new resource in the body, including the server-generated id and timestamps. If creation happens later, I return 202 Accepted with a Location that points to a status resource instead.
How should a client retry failed requests? (Senior signal)
Only when it is safe: network errors and timeouts on idempotent requests or requests with an idempotency key, 429 after waiting for Retry-After, and 503, 504 with backoff (and 500, 502 only carefully, a few times, for idempotent requests). Not 400, 404, 409, 422, which will fail again. I use exponential backoff with jitter and a maximum number of attempts, so many clients do not retry at the same moment.
Key takeaways
- Learn the table: GET is safe and idempotent. PUT and DELETE are idempotent. POST and PATCH are not, unless you design them to be.
- Idempotent means the same state after many calls, not the same response.
DELETEtwice gives204then404, and that is fine. - My retry test created two orders from one POST. A lost response plus a retry is the normal way duplicate data is born. Use
PUTwith a client id, a unique key, or anIdempotency-Key. - POST that creates returns
201andLocation. PUT replaces the whole object (my test lostqty). PATCH changes part of it. - Fifteen status codes cover almost everything.
401is "who are you",403is "not you",409is "state clash",412is "your version is old",422is "valid JSON, wrong values". - Use ETag and
If-Matchto stop two editors from overwriting each other. - Clients retry network errors,
429and503with backoff. They do not retry4xxerrors that will fail again.
What's next
Next: Request and Response Design, Lesson 2. Now that the methods and the status codes are right, we make the bodies right: consistent JSON, naming, dates and money, and one error format for the whole API, based on RFC 9457 "problem details". We will also run validation and show what an API must never return. Practise now: pick one POST in a project you know and write down what happens if it is sent twice.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- HTTP Semantics Interviewers Ask: Methods, Idempotency, Status Codes, Redirects · The Browser Contract
- REST API Design Best Practices: Resources, Versioning, Status Codes · Backend in Depth
- CORS Explained: Same-Origin Policy, Preflights and Credentials · The Browser Contract
Part of the study guide
Related series