
HTTP (HyperText Transfer Protocol) is the foundation of data communication on the web. It's a request-response protocol where a client (browser/app) sends a request, and a server sends back a response.
Key Characterstics:
Connectionless
When people say HTTP is connectionless, they don't mean your wifi drops after every request. They mean that by default, HTTP was designed to open a fresh TCP connection, send one request, get one response, and immediately close everything down. That's HTTP/1.0 in a nutshell.
This was fine when the web was just simple documents, but it became painful fast. Modern webpages need dozens of resources like HTML, CSS, JavaScript, images, fonts etc. Opening a new TCP connection for each one meant wasting 50-100ms on handshakes alone. That's why HTTP/1.1 introduced persistent connections with the Connection: keep-alive header, and HTTP/2 went further by shoving multiple requests through one connection simultaneously (multiplexing). The protocol is still "connectionless" at heart, either side can close whenever they want, but we've gotten smart about keeping pipes open when it makes sense.
The takeaway? HTTP doesn't maintain a persistent "session" at the transport level. Each request is theoretically independent, which is why we had to invent other ways to remember who you are (we'll get to that).
It's Stateless (The Server Has Amnesia)
This is the big one that confuses everyone initially. Stateless means the server treats every request like it's the first time you've ever talked. It doesn't remember that you logged in five seconds ago. It doesn't know you just added something to your cart. It looks at each request in complete isolation, processes it, sends a response, and forgets you ever existed.
At first this sounds insane. Why would you design something that forgets everything? But there's method to the madness. Stateless systems are incredibly easy to scale. If server #1 goes down, who cares? Send the next request to server #2, it doesn't need to know what server #1 was doing. No sticky sessions, no complex state synchronization, no "sorry, you need to reconnect to the same machine."
The downside is we have to fake statefulness. Ever wonder why websites make you send that session cookie with literally every single request? That's your proof of identity because the server sure doesn't remember you. JWT tokens work the same way, they encode your user info so the server can verify who you are without storing anything. It's like carrying your ID everywhere because the bouncer has no memory of your face.
It's Media Independent (It Doesn't Care What You Send)
HTTP is essentially a delivery truck. It doesn't care if you're shipping HTML documents, JSON APIs, cat videos, or executable files. It just moves bytes from point A to point B. The magic happens in the Content-Type header, which tells the recipient how to interpret those bytes.
This is why the same URL can return completely different things depending on what you ask for. Hit /users/123 with Accept: application/json and you get {"name": "John"}. Ask for Accept: text/html and you might get a full rendered profile page. The HTTP layer handles both identically. The difference is in how the server prepares the payload and how your browser parses it.
It's Client-Server (A Clear Split of Responsibilities)
HTTP enforces a specific relationship: clients ask, servers answer. Your browser or app is always the one initiating. The server sits there waiting, processing requests as they come in, never calling out to clients unbidden (WebSockets and server-sent events are different protocols layered on top).
This separation is powerful because it creates a clean contract. The server handles data, business logic, storage, and security. The client handles presentation, user input, and local state. Neither needs to know the other's internals. You can completely redesign your website without touching the API, or rewrite your backend in a different language without the mobile apps noticing.
I think of it like a restaurant. The menu (API contract) stays consistent even if the kitchen (server) gets new equipment or the dining room (client) gets renovated. As long as you can still order a burger and expect a burger, the system works.
It's Cacheable (Performance Through Reuse)
Perhaps HTTP's most under-appreciated feature is that responses can be stored and reused. The protocol has extensive mechanisms for controlling this - Cache-Control headers, ETags, Last-Modified dates, validation requests. A well-cached HTTP setup means your browser might never hit the origin server for static assets, pulling them instantly from local storage or a nearby CDN instead.
The immutable directive is my favorite example. When you see a URL like app.v2.js with Cache-Control: immutable, the browser knows that file will literally never change. It can cache it forever without even checking. Next time you load the page, that JavaScript appears instantly. If the code actually changes, the URL changes to app.v3.js and the cycle begins fresh.
This caching hierarchy:
browser → CDN → reverse proxy → origin
is what makes the modern web fast despite serving complex applications. HTTP's cacheability isn't an afterthought; it's a fundamental characteristic that shaped how we architect systems.
Why Any of This Matters
Understanding these characteristics isn't academic trivia. It explains why we do things that otherwise seem weird:
- Why cookies exist: Because HTTP is stateless and we need to identify you somehow
- Why JWTs became popular: Because they let us verify identity without server-side sessions, keeping things stateless and scalable
- Why API versioning is hard: Because URLs identify resources, and changing resource semantics breaks the contract
- Why caching bugs are maddening: Because HTTP allows multiple cache layers and invalidation is genuinely difficult
When you design APIs, you're working within these constraints. Stateless means every endpoint must be self-contained. Cacheable means you need to think about which responses can be stored and for how long. Media independent means your API can serve multiple clients if you design the representations thoughtfully.
HTTP Methods
GET: The Safe One
GET means "give me this resource.". It's read-only. You can GET the same URL a thousand times and nothing on the server changes. This safety is crucial because it means GET requests can be cached, bookmarked, shared in emails, and prefetched by browsers without consequences.
The constraints are real: GET requests shouldn't have side effects. Don't use GET to delete a user, even if you can technically make the URL /users/123/delete. Don't use GET to submit a form that changes data. I've seen production bugs where someone put a "log out" link as a GET request, and web crawlers or browser prefetching accidentally logged users out constantly.
GET parameters go in the URL, which means they're visible, have length limits (roughly 2000 characters depending on the browser), and get stored in browser history and server logs. Perfect for search queries, terrible for passwords or credit card numbers.
Real example:
GET /users?page=1&limit=10 HTTP/1.1
Host: api.example.comPOST: The Workhorse
POST is the catch-all "do something" method. Its primary job is creating new resources, but it's also used for any operation that doesn't fit neatly into other verbs. Submit a form? POST. Upload a file? POST. Trigger a complex calculation? POST. Even "delete but with extra confirmation data" sometimes ends up as POST because it's flexible.
POST is neither safe nor idempotent. Call it twice with the same data and you might get two different results, two orders, two users, two charges on a credit card. This is why browsers warn you about resubmitting forms when you hit the back button.
The body of a POST request carries the data, separate from the URL. This means no length limits, no visibility in browser history, and support for complex formats like multipart file uploads.
When POST succeeds at creating something, the server should return 201 Created with a Location header pointing to the new resource. I see too many APIs return 200 OK with the created object but no indication of where it lives now. That Location header matters for discoverability.
POST /users HTTP/1.1
Content-Type: application/json
{'{
"username": "john",
"email": "john@example.com"
}'}PUT: The Replace Operation
PUT confuses people because it sounds like "update" but actually means "replace this entire resource with what I'm sending you." When you PUT to /users/123, you're saying "here is the complete new state of user 123." If you omit a field, the server should typically null it out or set defaults. This is quite dangerous because you can accidentally override fields in your row you never intended to. If you include fields the server manages internally (like created_at), you're claiming authority over those too.
PUT is idempotent, which is its superpower. Send the same PUT request ten times, the result is identical to sending it once. The user has your exact data. This makes PUT reliable for retries. If you're unsure whether a previous request made it through (maybe the connection dropped), you can safely retry without creating duplicates.
The downside is you must send the complete resource. For large objects, this is wasteful. If you just want to change an email address, sending the entire user profile with 50 fields feels silly. That's where PATCH comes in.
PUT /users/123 HTTP/1.1
Content-Type: application/json
{'{
"id": 123,
"username": "john_updated",
"email": "new@example.com",
"bio": "New bio" // If omitted, bio becomes null/empty
}'}PATCH: The Surgical Strike
PATCH means "apply these specific changes to the resource." Unlike PUT's replacement strategy, PATCH is surgical. You send only the fields you want to modify, and everything else stays untouched.
Here's where it gets tricky: PATCH is not guaranteed to be idempotent. Whether it is depends entirely on how you implement it. If your PATCH simply sets email = 'new@example.com', that's idempotent. But if your PATCH says "append 'Jr.' to the last name" or "increment view count by 1," then calling it twice produces different results.
There are even competing standards for PATCH formats. The simple approach is JSON Merge Patch, just send the changed fields. The sophisticated approach is JSON Patch (RFC 6902), which uses an operations array: [{ "op": "replace", "path": "/email", "value": "new@example.com" }].
The operations format is more powerful for complex changes but harder to read and write.
I default to simple merge-style PATCH for most APIs unless I need the expressiveness of operations. Developers understand it immediately, and that's worth a lot.
PATCH /users/123 HTTP/1.1
Content-Type: application/json
{'{
"email": "new@example.com" // Only this field changes
}'}DELETE: Permanent Removal
DELETE is straightforward conceptually, remove this resource, but implementation details vary.
Some APIs return 204 No Content with an empty body, which is clean. Others return 200 OK with a copy of the deleted object, which helps clients who want to show "you just deleted X." Both are valid; just be consistent.
DELETE is idempotent. Delete user 123 once, they're gone. Delete again, they're still gone (same result). The server might return 404 on the second attempt since the resource no longer exists, but the system state is identical.
Soft deletes complicate this picture. If your DELETE actually sets a deleted_at timestamp rather than removing the row, is it truly idempotent? Technically yes, the state after first and second delete is the same (deleted_at is set). But now your GET might still find the resource with a filter.
DELETE /users/123 HTTP/1.1The Supporting Cast: HEAD, OPTIONS
These methods don't get enough attention but serve real purposes.
HEAD is identical to GET but returns only headers, no body. It's perfect for checking if a resource exists without downloading it, or checking Content-Length before deciding to download, or validating cache freshness with If-None-Match.
OPTIONS is the "what can I do here?" method. Browsers send it automatically as a "preflight" request before certain cross-origin requests to check if the actual request will be allowed. Your API can respond with Access-Control-Allow-Methods listing GET, POST, etc. It's also useful for API discovery, hit any endpoint with OPTIONS and learn what methods are available.
Practical Checklist for API Design
Before shipping an endpoint, ask:
- Is this read-only? Use GET and make it cacheable.
- Is this creating something new? Use POST, return 201 with Location.
- Is this replacing a complete resource? Use PUT, ensure idempotency.
- Is this a partial update? Use PATCH, document the format.
- Is this removing something? Use DELETE, handle soft-delete documentation.
- Does this trigger a side effect (email, charge, external API)? Consider POST.
- Do I need to check existence without downloading? Add HEAD support.
- Is this discoverable? Support OPTIONS with proper CORS headers.
HTTP Status Codes
Status codes are how your server talks back. Use them wrong and you break caching, confuse developers, or hide critical errors. I have not included 1xx status codes because they really aren't used most of the time.
2xx Success: The Happy Path
| Code | Name | When to Use | Common Mistake |
|---|---|---|---|
| 200 | OK | Generic success for GET, PUT, PATCH, DELETE | Using for POST that creates resources |
| 201 | Created | POST succeeded, new resource exists | Forgetting the Location header |
| 204 | No Content | Success with nothing to return (DELETE, empty updates) | Returning 200 with empty body instead |
| 206 | Partial Content | Range requests (video streaming, resumable downloads) | Not supporting when serving large files |
200 vs 201: I see this constantly. You POST to create a user, server creates it, returns 200 OK. Technically not wrong, but you missed a chance to signal "something new now exists." 201 tells the client "cache this, bookmark this, this is a new URL you can reference." It also pairs with the Location header, which saves the client from guessing where their new thing lives.
204 is underrated. When you DELETE a resource, what do you return? Some developers fetch the deleted object just to return it in the response. Others return { "success": true }. Both waste bytes. The client asked for deletion; they don't need proof of death. Return 204, empty body, connection closes. Clean.
206 saves bandwidth. When a user scrubs through a video timeline, they don't download the whole file. The player requests Range: bytes=0-1023, you return 206 with that chunk. Resume interrupted downloads the same way. Without 206, every video restart re-downloads from byte zero.
3xx Redirection: When Things Move
| Code | Name | Behavior | Production Use |
|---|---|---|---|
| 301 | Moved Permanently | Redirect, method becomes GET | SEO-critical URL changes, permanent renames |
| 302 | Found | Redirect, method becomes GET | Temporary redirects, but often misused |
| 307 | Temporary Redirect | Redirect, method preserved | POST stays POST, safe for form resubmission |
| 308 | Permanent Redirect | Redirect, method preserved | Permanent POST/PUT redirects (rare) |
| 304 | Not Modified | Use your cached version | Conditional GET responses, massive performance win |
4xx Client Errors: Your Fault, Fix It
| Code | Name | When to Return | What It Actually Means |
|---|---|---|---|
| 400 | Bad Request | Malformed syntax, invalid JSON, missing required fields | "I can't even parse what you sent me" |
| 401 | Unauthorized | No credentials or invalid credentials | "Who are you? Show me your ID!" |
| 403 | Forbidden | Authenticated but not permitted | "I know exactly who you are, and you're not allowed here" |
| 404 | Not Found | Resource doesn't exist | "This URL has nothing (or I'm not telling you if it does)" |
| 409 | Conflict | Request conflicts with current state | "That email is already taken" or "optimistic locking failed" |
| 422 | Unprocessable Entity | Valid syntax, semantic errors | "JSON is fine, but password is too short" |
| 429 | Too Many Requests | Rate limit exceeded | "Slow down, try again later" |
401 vs 403 is an interview classic for good reason. 401 means authentication failed—no token, expired token, wrong password. The response must include WWW-Authenticate header challenging the client to provide credentials. 403 means authentication succeeded but authorization failed. You proved you're John, but this admin area requires Jane. Returning 403 when you mean 401 breaks HTTP clients that expect to retry with credentials.
404 has security implications. Don't return 403 for hidden resources—that confirms something exists at that URL. Return 404 for both "doesn't exist" and "exists but you can't see it." This prevents user enumeration attacks where attackers probe /users/1, /users/2 to discover valid IDs.
5xx Server Errors: Our Fault, Wake Someone Up
| Code | Name | When It Happens | Severity |
|---|---|---|---|
| 500 | Internal Server Error | Unhandled exception, unexpected null, database connection lost | Critical, always alert |
| 502 | Bad Gateway | Upstream server returned invalid response | Usually infrastructure, check load balancer |
| 503 | Service Unavailable | Server overloaded, maintenance mode, circuit breaker open | Expected during deploys, scale events |
| 504 | Gateway Timeout | Upstream didn't respond in time | Database slow, external API hanging |
500 means you wrote a bug. Any unhandled exception becomes 500. Null pointer, missing environment variable, database constraint violation you didn't catch. These should trigger PagerDuty. Log the stack trace, but don't expose it to clients. Return generic { "error": "Internal server error", "request_id": "abc-123" } and correlate via request ID in logs.
HTTP Request Headers
| Header | Purpose | Example Value | When It Matters |
|---|---|---|---|
| Host | Which domain you're accessing | api.example.com or api.example.com:8080 | Required in HTTP/1.1+, enables virtual hosting (multiple sites on one IP) |
| Accept | What content formats you prefer | application/json, text/html;q=0.9 | Content negotiation—server picks best match |
| Accept-Encoding | Compression algorithms you support | gzip, deflate, br | Bandwidth reduction—server sends compressed response |
| Accept-Language | Preferred languages | en-US, fr;q=0.8, de;q=0.5 | Localization—server returns translated content |
| Authorization | Who you are | Bearer eyJhbG... or Basic dXNlcjpwYXNz | Authentication—required for protected resources |
| Content-Type | Format of the body you're sending | application/json; charset=utf-8 | Required for POST/PUT/PATCH—tells server how to parse body |
| Content-Length | Size of body in bytes | 348 | Server knows when request is complete |
| Cookie | Stored session data | session=abc123; theme=dark | State management—server identifies returning users |
| User-Agent | What software is making the request | Mozilla/5.0 (Windows NT 10.0...) | Analytics, browser-specific workarounds, rate limiting |
| Referer | Previous page URL | https://google.com/search?q=example | Analytics, CSRF protection (note: misspelled in spec) |
| If-None-Match | Conditional GET with ETag | "33a64df5" | Caching—"only send if changed" |
| If-Modified-Since | Conditional GET with date | Wed, 21 Oct 2024 07:28:00 GMT | Caching alternative to ETag |
| Origin | Where the request comes from (CORS) | https://example.com | Security—server decides if cross-origin request allowed |
| X-Request-ID | Unique trace identifier | 550e8400-e29b-41d4-a716-446655440000 | Distributed tracing—correlate logs across services |
Accept Headers:
The server uses these to decide what to send back. An API might ignore everything except Accept: application/json. A multilingual site might serve French content based on Accept-Language. The q values (quality factors, 0-1) indicate preference priority.
Authorization:
The Authorization header carries credentials. Despite its name, it's for authentication (proving identity), not authorization (checking permissions). That confusion persists because the spec got it wrong decades ago. E.g
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...The token proves who you are. The server validates signature, extracts claims, checks expiration. Stateless, no database lookup required for JWTs.
Conditional Headers: The Performance Hack
If-None-Match and If-Modified-Since enable conditional requests. Client has cached resource, asks "send me this only if changed."
GET /logo.png HTTP/1.1
If-None-Match: "abc123"
Server checks: ETag still "abc123"?
Yes → HTTP/1.1 304 Not Modified (no body, 50 bytes)
No → HTTP/1.1 200 OK with full image (50KB)HTTP Response header
| Header | Purpose | Example Value | When It Matters |
|---|---|---|---|
| Content-Type | Format of returned body | application/json; charset=utf-8 | Client knows how to parse—critical for correct rendering |
| Content-Length | Size of body in bytes | 1256 | Progress bars, memory allocation, connection management |
| Content-Encoding | Compression applied | gzip or br (Brotli) | Client must decompress before parsing |
| Transfer-Encoding | How body is transferred | chunked | Streaming when total size unknown |
| Location | Where to redirect or find resource | /users/123 or https://new.com/page | Required with 201, 301, 302, 307, 308 |
| Set-Cookie | Store data in browser | session=abc; HttpOnly; Secure; SameSite=Strict | Session management, tracking, preferences |
| Cache-Control | Caching instructions | max-age=3600, private, must-revalidate | Performance—controls all cache layers |
| ETag | Entity tag for validation | "33a64df551425fcc55e" | Conditional requests, cache freshness |
| Last-Modified | When resource last changed | Wed, 21 Oct 2024 07:28:00 GMT | Alternative to ETag, weaker but simpler |
| Expires | Absolute expiration date | Thu, 01 Dec 2024 16:00:00 GMT | Legacy caching—Cache-Control preferred |
| WWW-Authenticate | Auth challenge | Bearer realm="api", scope="read write" | Required with 401—tells client how to auth |
| Retry-After | When to retry (seconds or date) | 120 or Wed, 21 Oct 2024 08:00:00 GMT | Rate limiting (429), maintenance (503) |
| Access-Control-Allow-Origin | CORS permission | * or https://example.com | Cross-origin requests—security critical |
| Strict-Transport-Security | Force HTTPS | max-age=31536000; includeSubDomains | Prevents downgrade attacks—HSTS |
| X-Content-Type-Options | Prevent MIME sniffing | nosniff | Security—browser must respect Content-Type |
| X-Frame-Options | Clickjacking protection | DENY or SAMEORIGIN | Prevents your site in iframes on malicious sites |
| Server | Server software identification | nginx/1.18.0 or cloudflare | Information leakage—often removed in production |
Content Description Headers
These tell the client what they're receiving and how to handle it.
Content-Type is mandatory. It declares the format, application/json, text/html, image/png. Without it, browsers guess, often incorrectly. Always include charset for text: application/json; charset=utf-8. A missing or wrong charset turns emojis into gibberish.
Content-Length states the body size in bytes. Browsers use this for progress bars and memory allocation. Omit it for chunked transfers when streaming unknown amounts of data.
Content-Encoding reveals compression, gzip, br (Brotli), or deflate. The client must decompress before parsing. Brotli beats gzip by 20-30% but requires HTTPS browser support.
###Navigation and Location Headers
Location serves two purposes. With 201 Created, it points to the new resource: Location: /users/123. With 301, 302, 307, or 308, it tells the browser where to redirect. This header changes everything, bookmark it, cache it, follow it.
Caching Headers
Cache-Control is your performance dial. max-age=3600 means fresh for one hour. private restricts to browser only, critical for user data. immutable promises the content never changes, enabling aggressive caching. Combine directives: public, max-age=31536000, immutable for versioned assets.
ETag provides a content fingerprint: ETag: "abc123". Clients send If-None-Match: "abc123" on subsequent requests. If content unchanged, you return 304 Not Modified with no body. A one-byte header replaces a megabyte response.
Last-Modified offers a weaker alternative, timestamps instead of hashes. Easier to generate but less precise. Use ETags when possible.
Expires sets absolute expiration dates. Legacy support only, prefer Cache-Control.
State Management Headers
Set-Cookie instructs browsers to store data. Session tokens, preferences, tracking IDs, all live here. Attributes matter: Secure (HTTPS only), HttpOnly (JavaScript cannot access), SameSite=Strict (never sent cross-origin). Omit these and you invite XSS and CSRF attacks.
Security Headers
These protect users from common attacks.
Strict-Transport-Security forces HTTPS. max-age=31536000; includeSubDomains tells browsers "never use HTTP for this domain for one year." Prevents SSL stripping attacks where intermediaries downgrade connections.
X-Content-Type-Options: nosniff stops browsers from guessing MIME types. Upload a malicious file named photo.jpg containing HTML, without this header, some browsers render it as a web page. With it, they respect your declared Content-Type.
X-Frame-Options: DENY prevents your site from appearing in iframes on other domains. Stops clickjacking attacks where malicious sites overlay invisible buttons on your content.
Content-Security-Policy restricts resource loading. default-src 'self' blocks external scripts, images, and styles unless explicitly permitted. Complex to configure but stops XSS cold.
Cross-Origin Headers
Access-Control-Allow-Origin controls CORS. * allows any site to call your API—dangerous for authenticated endpoints. Specific origins like https://example.com restrict access. Preflight OPTIONS requests check these permissions before the actual request.
Rate Limiting and Retry Headers
Retry-After tells clients when to return. Send 120 (seconds) or a specific date with 429 Too Many Requests or 503 Service Unavailable. Without it, clients guess and often hammer your servers harder.
WWW-Authenticate accompanies 401 Unauthorized. It challenges the client: Bearer realm="api" indicates token-based auth required. Clients parse this to know how to authenticate.
Observability Headers
X-Request-ID traces requests across distributed systems. Generate at the edge, propagate through every microservice, log everywhere. One ID correlates logs from load balancer to database. Without it, debugging production issues becomes archaeology.
Server Identification Headers
Server reveals software: nginx/1.18.0, cloudflare, Microsoft-IIS/10.0. Information leakage, attackers target known vulnerabilities. Many production systems strip or falsify this.
Transfer Headers
Transfer-Encoding: chunked streams data of unknown size. Server sends chunks with size prefixes, client assembles them. Essential for live data, generated reports, or any response too large to buffer in memory. HTTP/2 and HTTP/3 use this internally even when not explicitly set.
Related Articles
Understanding Golang Packages And Modules
Go’s simplicity hides powerful concepts like packages and modules that make large-scale applications maintainable and efficient. In this guide, we break down how packages structure your code and how modules handle dependencies in modern Go development.

REST APIs: Beyond the Buzzwords
Stop guessing how to structure your endpoints. We break down the core principles of RESTful design and explain why some "rules" are made to be broken in production.

Claude Code Source Leak: GitHub Repo, What’s Inside, and What Happened
Looking for the Claude Code GitHub repository or the leaked source from February 2025? Here are the exact mirrors, what they contain, and the story behind how a debugging source map accidentally exposed the internals of Anthropic’s Claude Code tool.

The Axios Hack 2026: What Happened and What You Need to Know
On March 31, 2026, attackers briefly compromised Axios, a tool used in millions of websites. Here's what happened in plain English, and what you should check right now.

The Complete API Architecture Guide: REST, GraphQL, gRPC, tRPC, WebSockets & SSE
Navigate the complex landscape of API architectures with data-driven insights. From REST's reliability to gRPC's 10x performance gains, understand which protocol fits your use case, team structure, and scalability requirements.