Both messages have the same shape
A start line, then headers, then a blank line, then an optional body. That is the entire format. HTTP/2 and HTTP/3 compress the headers into binary frames, but the model — this start line, these headers, this body — is unchanged, which is why they are compatible.
POST /api/orders HTTP/1.1
Host: api.doors.test
Content-Type: application/json
Content-Length: 41
Authorization: Bearer eyJhbGciOi…
{"door":"vesper","qty":2,"discount":10}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/482
Cache-Control: no-store
{"id":482,"status":"quoted"}
Methods, and the two properties that matter
Safe means it changes nothing, so a crawler or a prefetcher may call it without asking. A "delete" link behind a GET will be clicked by a robot eventually — this is not hypothetical, it has taken down real admin panels.
Idempotent means calling it twice has the same effect as once, which is what lets a browser or a proxy retry safely after a timeout. POST is the one that cannot be retried blindly, which is why payment endpoints take an idempotency key.
| Method | Safe | Idempotent | Use |
|---|---|---|---|
| GET | yes | yes | read something; never change anything |
| HEAD | yes | yes | the headers only — is it there, how big |
| POST | no | no | create, or anything that is not the others |
| PUT | no | yes | replace a thing at a known address |
| PATCH | no | no | change part of a thing |
| DELETE | no | yes | remove it |
| OPTIONS | yes | yes | what is allowed here (and CORS preflights) |
Status codes, in families
| Family | Means | The ones you will actually use |
|---|---|---|
| 1xx | hold on | 101 Switching Protocols (WebSocket) |
| 2xx | it worked | 200 OK · 201 Created · 204 No Content |
| 3xx | look elsewhere | 301 · 302 · 304 Not Modified · 307 · 308 |
| 4xx | your request is wrong | 400 · 401 · 403 · 404 · 409 · 422 · 429 |
| 5xx | the server broke | 500 · 502 · 503 · 504 |
401 Unauthorized means "I do not know who you are" — log in and try again, and the response must say how with a WWW-Authenticate header. 403 Forbidden means "I know exactly who you are, and no". Sending 401 for a permission problem makes a client log the user out for no reason; sending 403 for a missing token stops them from ever logging in.
The codes that save you an argument
- 204 No Content — it worked and there is nothing to say. A DELETE that returns
{}with a 200 is just noise. - 409 Conflict — the request was fine, the state was not: a duplicate email, an edit to something somebody else changed.
- 422 Unprocessable Content — the body parsed but failed validation, where 400 means it did not even parse.
- 429 Too Many Requests — with
Retry-After, so a client can back off instead of guessing. - 503 Service Unavailable — deliberate and temporary, also with
Retry-After. Not 500, which means "something broke and we do not know what".
Bodies, and what a client does with them
const res = await fetch("/api/orders", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ door: "vesper", qty: 2 }),
});
// fetch only rejects on a network failure. 404 and 500 are perfectly good responses.
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const order = await res.json(); // reading the body is the second await
Forgetting the check is the most common bug in code that calls an API: res.json() on a 500 that returned an HTML error page throws a confusing parse error, and the real status is lost.