HTTP, Deeply
Stage 3 · Backend & APIs · B.U.I.L.D. letter: U
Every bug where your API "just doesn't work" is a bug in your mental model of HTTP — master the protocol and the mystery disappears.
⚠️ The vibe trap
You've shipped a fetch() call and it worked, so HTTP feels solved. It isn't. When you're on the receiving end — writing the server — you have to honour every clause of the protocol yourself: the right method, the right status code, the right headers. Get any one wrong and clients will cache responses they shouldn't, retry requests that aren't safe to retry, or silently swallow errors you meant to be loud. HTTP is a contract. You have to sign it properly.
📬 Anatomy of a Request
Every HTTP request is plain text sent over a TCP connection. It has three parts: a request line, headers, and an optional body.
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
Content-Length: 42
{"name": "Aaliyah Brooks", "email": "a@example.com"}
- Request line — method + path + HTTP version. That's it.
- Headers — key/value pairs, one per line, blank line after the last one.
- Body — anything after that blank line. GET requests conventionally have no body.
Mental model: think of a request as a sealed envelope. The outside (method + path + headers) tells the post office where to send it and what's inside. The body is the letter itself. The post office (network, proxies, CDNs) reads the envelope; your server reads the letter.
Common mistake: Sending a body with a GET request. Some servers ignore it, some reject it, and HTTP caches will never store a GET-with-body response correctly. If you need to send data, use POST.
📨 Anatomy of a Response
The server replies with a status line, headers, and an optional body.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/9f3a
Cache-Control: no-store
{"id": "9f3a", "name": "Aaliyah Brooks", "email": "a@example.com"}
- Status line — HTTP version + status code + reason phrase.
- Headers — tell the client how to interpret the body, whether to cache it, where to redirect, etc.
- Body — the actual resource. Can be JSON, HTML, binary, or empty.
Mental model: if the request is a question, the response is both the answer and the instructions for what to do with the answer. The status code is "here's my verdict"; the headers are "here's how to handle it"; the body is "here's the thing you asked for."
Common mistake: Returning a 200 OK with
{"error": "not found"}in the body. This breaks every HTTP-aware tool — caches will store it, monitoring will show green, and clients have to parse the body just to discover failure. Use the correct status code.
🔧 HTTP Methods and Their Contracts
Each method carries a semantic promise that every layer of the internet — browsers, CDNs, proxies, retry logic — depends on.
| Method | Safe? | Idempotent? | Use for |
|---|---|---|---|
| GET | ✅ yes | ✅ yes | Retrieve a resource |
| HEAD | ✅ yes | ✅ yes | Same as GET, headers only |
| POST | ❌ no | ❌ no | Create a new resource or trigger an action |
| PUT | ❌ no | ✅ yes | Replace a resource entirely |
| PATCH | ❌ no | ❌ no* | Partially update a resource |
| DELETE | ❌ no | ✅ yes | Remove a resource |
Safe means "no side effects." A CDN will automatically cache GET. A browser will prefetch GET links. Neither will do that for POST.
Idempotent means "calling it N times has the same result as calling it once." This is what makes retry logic safe. If a DELETE request times out and the client retries, the resource is still gone — the second call is a no-op. That's idempotent. A POST to /orders would create a second order — not idempotent.
# Demonstrate idempotency: run this twice — second call returns 404, not a second deletion
curl -X DELETE https://httpbin.org/delete \
-H "Content-Type: application/json" \
--verbose
Common mistake: Using POST for updates because "it's easier." The moment you do this, every retry mechanism in the client stack (Fetch API retries, service workers, load balancers) will potentially double-execute your update. Use PUT for full replacement, PATCH for partial — and mean it.
🚦 Status Codes You Must Know
Status codes are grouped by their first digit. Here are the ones you will use or encounter on every project:
2xx — Success
200 OK — Request succeeded. Body contains the resource.
201 Created — POST succeeded and created a new resource. Include a Location header.
204 No Content — Success, but nothing to return (e.g. a DELETE or a state-change).
3xx — Redirection
301 Moved Permanently — Resource is at a new URL forever. Clients (and Google) will update.
302 Found — Temporary redirect. Client should keep using the original URL.
304 Not Modified — Client's cached version is still valid. Send no body.
4xx — Client error (you sent something wrong)
400 Bad Request — Malformed syntax, unparseable body, missing required field.
401 Unauthorized — No valid credentials. (Despite the name: "not authenticated.")
403 Forbidden — Valid credentials, but you don't have permission. ("Not authorised.")
404 Not Found — The resource doesn't exist at this path.
409 Conflict — Request can't be completed because of a conflict (e.g. duplicate email).
422 Unprocessable — Body is syntactically valid but semantically wrong (failed validation).
429 Too Many Requests — Rate limit hit. Include a Retry-After header.
5xx — Server error (we did something wrong)
500 Internal Server Error — Unhandled exception. Never leak stack traces in this response.
502 Bad Gateway — Your server got a bad response from an upstream service.
503 Service Unavailable — Temporarily down. Maintenance, overload. Include Retry-After.
# See the exact status code a real endpoint returns
curl -o /dev/null -s -w "%{http_code}\n" https://httpbin.org/status/404
# → 404
curl -o /dev/null -s -w "%{http_code}\n" https://httpbin.org/status/201
# → 201
Mental model: the first digit is the chapter; the last two digits are the verse. Read the chapter first — that tells you whose fault it is and whether to retry. Then read the verse for specifics.
Common mistake: Returning 200 for a failed login (because "the request itself worked"). A failed login is a client error. Return 401. Returning 200 makes every monitoring tool, every API client, and every log aggregator blind to auth failures.
🏷️ Headers That Matter Most
Headers are the metadata layer of HTTP. Three categories you need to own immediately:
Content negotiation
Content-Type: application/json # What I'm sending you
Accept: application/json # What I want back
Authentication
Authorization: Bearer <jwt-token>
Authorization: Basic <base64(user:pass)>
Caching
Cache-Control: no-store # Never cache this (auth endpoints, personal data)
Cache-Control: max-age=3600 # Cache for one hour
Cache-Control: no-cache # Revalidate before using the cache
ETag: "33a64df551425fcc55e" # Fingerprint of the resource version
Here is a complete curl command that sets all three, so you can see them sent together:
curl -X GET https://api.example.com/me \
-H "Accept: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.test.sig" \
-H "Cache-Control: no-cache" \
--verbose
The --verbose flag prints the full request and response headers — run it against your own app and read every line.
Common mistake: Forgetting
Content-Type: application/jsonon POST/PUT/PATCH requests. Express, Fastify, and most frameworks will not parse the body correctly without it. The body arrives as a raw string and yourreq.bodyis empty or undefined. Always set Content-Type when sending a body.
🔒 Statelessness — What It Actually Means
HTTP is stateless: every request must carry all the information the server needs to respond. The server remembers nothing between requests. No session, no context, no "remember what I told you last time."
This is not a limitation — it's what makes HTTP scalable. Ten servers behind a load balancer can each handle any request from any client because the request is self-contained.
// BAD — relies on server-side memory between calls
// Call 1: POST /login → server stores "user X is logged in" in RAM
// Call 2: GET /me → server looks up "who is logged in" from RAM
// → BREAKS on a second server with no shared RAM
// GOOD — stateless: each request is self-contained
// Call 1: POST /login → server returns a JWT token
// Call 2: GET /me → client sends JWT in Authorization header
// → any server can verify the JWT independently
Mental model: imagine every request arriving as a cold case file with no prior history. Your server must be able to rule on it using only what's in the envelope. If it can't, the client isn't sending enough information — not the server's problem to compensate.
Common mistake: Storing user session data in a server-side variable (e.g. a module-level object in Node.js). Works fine locally with one server, breaks silently the moment you deploy more than one instance. Use JWTs or a shared session store (Redis, a DB) instead.
🛠️ Your Mission
Take an endpoint in the app you're building right now and audit it against everything in this lesson.
-
Capture the raw conversation. Run this command and paste the full output into a comment or notes file:
curl -X POST http://localhost:3000/api/YOUR_ENDPOINT \ -H "Content-Type: application/json" \ -d '{"test": true}' \ --verbose -
Check every field in the response:
- Is the status code actually correct for what happened? (201, not 200, for a created resource.)
- Is
Content-Type: application/jsonpresent on the response? - Does a failed auth return 401, not 200 + error body?
-
Fix one wrong status code you find. Commit the change with a message like
fix(api): return 201 on user creation. -
Prove statelessness: add a second
console.logthat would break if the server forgot the previous request. Then verify it still works when you restart the server between requests.
✅ You're done when…
- You can label every line of a raw HTTP request and response by name (method, path, header, body, status line).
- You can explain, without notes, why POST is not idempotent and DELETE is.
- Your audit found at least one status code to fix, and you fixed it (API Design Checklist).
- You ran
curl --verboseand read the full output without skipping the headers section. - You can explain statelessness to someone else in one sentence (Production-Readiness Checklist).
➡️ Next: Designing a REST API. Build It Right, Or Don't Build It At All. 🏛️