All articles
Backend from First Principles

Understanding HTTP: A Mental Model for Backend Engineers

Every backend system sits on top of HTTP. Let's build a mental model that makes every concept click into place — from methods and headers to CORS and caching.

2025-12-1511 min read

Every backend system you'll ever build sits on top of HTTP. Yet most engineers treat it as a black box. Let's change that.

I'm revisiting backend fundamentals from first principles, and I started where everything starts — HTTP. Instead of memorizing RFCs, I built a mental model that makes every concept click into place. Here's what I distilled.


The Foundation: Two Rules That Govern Everything

Every HTTP concept — methods, headers, status codes, caching, CORS — traces back to two foundational rules. Internalize these, and the rest becomes intuitive.

Rule 1: Statelessness — "Every conversation starts from scratch."

The server has perfect amnesia. It doesn't remember your last request, your login, or your preferences. Every single request must carry everything the server needs to process it — authentication tokens, session context, all of it.

This sounds like a limitation. It's actually a superpower:

  • Scalability: If no server remembers you, any server can handle you. Spin up 50 instances behind a load balancer and it just works.
  • Fault tolerance: A server crash destroys zero client state. Retry the request and move on.
  • Simplicity: No complex session-tracking machinery on the server side.

The nuance: stateless doesn't mean no state exists. State lives in databases, in Redis, in JWT tokens the client carries around. HTTP is the courier, not the warehouse.

Rule 2: Client-Server — "The server never speaks first."

The server is a shopkeeper who never calls you. It waits. The client always initiates. This clean separation means the client owns the experience, and the server owns the data and logic.


The Pipe: How HTTP Gets From A to B

HTTP needs a reliable delivery system underneath. That's TCP — it guarantees ordered, error-checked delivery.

The evolution of HTTP is really about how efficiently we use this pipe:

  • HTTP/1.0 — Open a new connection for every single request. Like driving to the post office for each individual letter.
  • HTTP/1.1 — Persistent connections (keep-alive). Drive once, hand over multiple letters through the window.
  • HTTP/2 — Multiplexing. Hand over multiple letters simultaneously through multiple windows. Plus header compression and binary framing.
  • HTTP/3 — Replace TCP entirely with QUIC (built on UDP). Faster connections, no head-of-line blocking. Like switching from road delivery to drones.

The key insight: every version keeps the same HTTP semantics. Methods, headers, and status codes don't change. Only the transport efficiency improves. Your application code barely notices the difference.


The Envelope: Anatomy of Every HTTP Message

Every HTTP interaction is a pair — one request, one response. Both follow the same four-part structure:

[Start Line]    — What is this? (method + URL, or status code)
[Headers]       — Metadata about the message
[Blank Line]    — "I'm done with metadata"
[Body]          — The actual payload (optional)

That blank line isn't decoration. It's the delimiter between metadata and content. Without it, the parser wouldn't know where headers end and the body begins.


Headers: The Remote Control

Here's the mental model that makes headers click: headers are a remote control for the server and the browser.

The body carries what you're sending. Headers tell the system how to handle it. This is why you don't shove everything into the body — a mail carrier needs to read the address label without opening the package.

Four categories to remember:

  • Request headers — Client introduces itself. "Here's who I am (User-Agent), my credentials (Authorization), and what I'd prefer (Accept)."
  • Response headers — Server gives instructions. "Cache this for 3600 seconds (Cache-Control). Here's a cookie to carry back (Set-Cookie)."
  • Representation headers — Describe the body. "The content is JSON (Content-Type), it's 2048 bytes (Content-Length), compressed with gzip (Content-Encoding)."
  • Security headers — Enforce rules. "Only use HTTPS (Strict-Transport-Security). Don't run untrusted scripts (Content-Security-Policy). Don't embed this page (X-Frame-Options)."

The killer feature? Headers are extensible. You can create X-App-Version: 2.4.1 and pass domain-specific metadata without breaking the protocol. This extensibility is why HTTP has survived 30+ years.


Methods: Declaring Intent

HTTP methods declare what you intend to do, not what the server must do.

  • GET — "Show me this resource"
  • POST — "Here's something new, create it"
  • PUT — "Replace this resource entirely"
  • PATCH — "Update just these fields"
  • DELETE — "Remove this"
  • OPTIONS — "What can I do here?"

Idempotency: The Concept That Saves You in Production

Ask yourself: "If this request accidentally fires twice, is the world still correct?"

  • GET /users/5 twice — Same user returned. No harm. Idempotent.
  • PUT /users/5 {name: "Ali"} twice — Same replacement. Same result. Idempotent.
  • DELETE /users/5 twice — First deletes, second gets 404. Server state unchanged. Idempotent.
  • POST /orders {item: "shoe"} twice — Two orders created. Not idempotent. Dangerous.

Why this matters: networks are unreliable. Requests get retried by clients, load balancers, and retry middleware. Idempotent endpoints are safe to retry. Non-idempotent ones need protection — idempotency keys, deduplication, or database constraints.


CORS: The Browser's Bodyguard

This one confuses almost everyone because it's not a server feature — it's a browser feature.

Mental model: your browser is an overprotective parent.

Your frontend at app.example.com tries to call api.different.com. The browser intervenes: "That's a different origin. Let me verify this is safe."

Same Origin = same protocol + same domain + same port. Anything different triggers CORS.

Simple requests (basic GET/POST with standard headers): The browser adds an Origin header. If the server responds with Access-Control-Allow-Origin matching your domain — you're in. If not, the browser blocks the response.

Preflight requests (PUT/DELETE, custom headers, JSON content-type): The browser fires an OPTIONS request first — "Can you handle PUT with Authorization headers from this origin?" The server responds with what it allows. Only then does the actual request proceed.

The critical insight: CORS doesn't protect the server. The server processes the request regardless. CORS protects the user by preventing malicious JavaScript from reading cross-origin responses. This is exactly why your API works fine from Postman or curl but breaks from a webpage.


Status Codes: The Universal Contract

Don't memorize all of them. Internalize the ranges:

  • 1xx — "Hold on..." (Informational, rarely used directly)
  • 2xx — "All good." (Happy path)
  • 3xx — "Go look over there." (Redirects & caching)
  • 4xx — "You messed up." (Client error)
  • 5xx — "I messed up." (Server error)

The ones you'll reach for daily:

Success: 200 OK | 201 Created (after POST) | 204 No Content (after DELETE)

Redirection: 301 Moved Permanently | 302 Temporary Redirect | 304 Not Modified (use your cache)

Client errors: 400 Bad Request | 401 Unauthorized (who are you?) | 403 Forbidden (I know you, but no) | 404 Not Found | 409 Conflict (duplicate entry) | 429 Too Many Requests (rate limited)

Server errors: 500 Internal Server Error (unhandled crash) | 502 Bad Gateway (proxy can't reach upstream) | 503 Service Unavailable (down for maintenance) | 504 Gateway Timeout

Pro tip: The 401 vs 403 distinction is a classic. 401 = authentication failure ("I don't know you"). 403 = authorization failure ("I know you, but you can't do this"). Getting this right in your APIs signals backend maturity.


Caching: The Performance Multiplier

HTTP caching is a conversation between client and server about freshness.

First request — Server returns data along with: Cache-Control: max-age=3600 (valid for one hour), ETag: "abc123" (fingerprint of the data), and Last-Modified (timestamp).

Within the cache window — The browser doesn't even contact the server. It serves from local cache. Zero latency.

After cache expires — The client sends a conditional request: "I have version abc123. Has anything changed?" (If-None-Match: "abc123"). If unchanged, the server replies with just 304 Not Modified — no body, massive bandwidth savings. If changed, it sends the new data with a fresh ETag.

The production reality: cache invalidation is genuinely hard. Wrong max-age = users see stale data. Wrong ETag = unnecessary re-downloads. This is why most teams use versioned URLs for static assets (/bundle.a3f8c2.js) — cache forever, change the URL when the content changes.


Content Negotiation & Compression

Client and server politely negotiate format, language, and encoding:

Client: "I'd prefer JSON, in English, compressed with gzip"
         Accept: application/json
         Accept-Language: en
         Accept-Encoding: gzip

Server: "Here's JSON, in English, compressed with gzip"
         Content-Type: application/json
         Content-Encoding: gzip

Compression is a free performance win. A 26MB JSON payload shrinks to ~3.8MB with gzip — 85% bandwidth reduction, minimal CPU cost. In production, your reverse proxy (nginx) handles this transparently.


Handling Large Data: When Normal Isn't Enough

Large uploads (client to server): JSON can't handle binary data efficiently. Use multipart/form-data — it splits the upload into chunks separated by a boundary delimiter. Each chunk carries its own mini-headers. Think of it like a book with chapter dividers.

Large downloads (server to client): Instead of making the client wait for the entire response, the server streams data in chunks using Transfer-Encoding: chunked or Server-Sent Events. The client assembles pieces as they arrive. This is how ChatGPT streams its responses.


HTTPS & TLS: The Secure Envelope

The simplest mental model: HTTPS = HTTP inside a tamper-proof, encrypted envelope.

TLS (the modern replacement for SSL) provides three guarantees:

  1. Encryption — Nobody can read the data in transit
  2. Authentication — The server proves its identity via certificates
  3. Integrity — Nobody can tamper with the data undetected

The TLS handshake happens before any HTTP data flows. Once the secure channel is established, HTTP works identically — it just runs through an encrypted tunnel.


The Takeaway

HTTP is elegantly simple at its core: a client sends a structured request, a server sends a structured response. Every feature — methods, headers, status codes, CORS, caching, compression, TLS — exists to solve a specific real-world problem on top of that loop.

Understanding HTTP isn't about memorizing specs. It's about building a mental model where every concept has a reason for existing and a clear place in the architecture.

This is where backend engineering starts. Everything else — REST APIs, authentication flows, WebSockets, microservice communication — builds directly on top of this foundation.


Everything in this article comes from what I learned watching the Backend from First Principles playlist by Sriniously — specifically the video "Understanding HTTP for backend engineers, where it all starts." I'm not claiming original research here. This is me taking what I studied, internalizing it, and presenting it as a mental model that made the concepts stick for me. If it helps you too, even better.

Playlist: https://youtube.com/playlist?list=PLui3EUkuMTPgZcV0QhQrOcwMPcBCcd_Q1

Follow along for the next one.

#BackendEngineering #HTTP #WebDevelopment #SoftwareEngineering #SystemDesign #LearningInPublic