HTTP is just structured plain text sent over a TCP connection — a method, a path, some headers, an optional body. Once that's visible directly, status codes, headers, and caching stop being memorized trivia and start being one coherent format.
3 min read
Strip away every library and framework, and an HTTP request is literally this, sent as plain text over an already-open TCP connection:
GET /users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOi...
And the response, the exact same shape back:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 57
{"id": 42, "name": "Ada Lovelace", "role": "engineer"}
Every REST API, every fetch() call, every browser page load reduces to exactly
this: a start line, headers, a blank line, then an optional body. curl -v shows
you this literal exchange for any real request, headers and all — genuinely
worth running once against a real API to see it directly instead of only through
a library's abstraction.
The GET-never-changes-state rule is a real contract, not a suggestion — browsers prefetch GET requests, proxies cache GET responses, and retry logic assumes GET is always safe to repeat. A GET endpoint that deletes data on the server violates that contract and will eventually break in a way that's genuinely hard to trace back to "someone's cache prefetched a delete."
2xx — success 200 OK, 201 Created, 204 No Content
3xx — redirect 301 Moved Permanently, 304 Not Modified
4xx — client's request is wrong 400 Bad Request, 401 Unauthorized, 404 Not Found
5xx — server's fault 500 Internal Server Error, 503 Service Unavailable
The 4xx/5xx line matters more than it looks: a 4xx means the client should change something before retrying (fix the request, log in, use a valid ID) — a 5xx means the client did nothing wrong, the server itself failed, and a plain retry might actually succeed. Client code that blindly retries every failure the same way, or that never retries anything, is missing this distinction.
A handful you'll see constantly:
Content-Type — what format the body is in (application/json, text/html, multipart/form-data)Content-Length — how many bytes the body is, so the receiver knows when it's fully arrivedAuthorization — credentials, almost always Bearer <token> for APIs todayCache-Control — how long, and whether, a response may be cached (the actual mechanism behind the "clear your cache" folk wisdom)User-Agent — what client is making the request (a browser, a script, a specific app version)Each HTTP request is independent — the server keeps no memory of a previous
request from the same client unless the client explicitly resends identifying
information (a cookie, a token in Authorization) every single time. This is
deliberate: because no server-side session state is required between requests,
any request can be routed to any server behind a load balancer, and any server
can be added, removed, or restarted without breaking anyone's "session" — a
property distributed systems depend on constantly, covered directly in the
System Design domain.
An operation is idempotent if doing it once and doing it five times produce
the same end state. PUT /users/42 {"name": "Ada"} is idempotent — repeating it
just sets the same name again. POST /orders typically isn't — repeating it
naively creates five orders. This distinction is exactly why retry logic needs to
know which method it's retrying: safely auto-retrying a failed GET or PUT is
usually fine; auto-retrying a failed POST without an idempotency key can
duplicate real side effects (charging a card twice is the classic example, and
comes up directly again in the Payments & Financial Systems domain).
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why should a GET request never change server-side state?
2. What's the key practical difference between a 4xx and a 5xx status code?
3. Why is HTTP's statelessness important for scaling a system behind a load balancer?
Networking Fundamentals