The request and the response

Read a request and a response as the text they are, and choose the right method and status.

03of 18
≈ 12Min read
5Parts

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.

a request
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}
and a response
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.

MethodSafeIdempotentUse
GETyesyesread something; never change anything
HEADyesyesthe headers only — is it there, how big
POSTnonocreate, or anything that is not the others
PUTnoyesreplace a thing at a known address
PATCHnonochange part of a thing
DELETEnoyesremove it
OPTIONSyesyeswhat is allowed here (and CORS preflights)

Status codes, in families

FamilyMeansThe ones you will actually use
1xxhold on101 Switching Protocols (WebSocket)
2xxit worked200 OK · 201 Created · 204 No Content
3xxlook elsewhere301 · 302 · 304 Not Modified · 307 · 308
4xxyour request is wrong400 · 401 · 403 · 404 · 409 · 422 · 429
5xxthe server broke500 · 502 · 503 · 504
401 is not 403

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

fetch is two steps, and the first one does not throw
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
res.ok is status 200–299

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.

Practice