Chapter 9: HTTP Deep Dive: Methods, Status Codes, Headers, and the Request-Response Lifecycle
Chapter 9: HTTP Deep Dive: Methods, Status Codes, Headers, and the Request-Response Lifecycle
Why This Chapter Matters
In the previous chapters, we built an Express server from scratch, created standard response and error utilities, and locked down incoming request validation with DTOs. Along the way, we used things like GET, POST, res.status(200), and Content-Type: application/json—but we didn't stop to deeply understand any of them.
HTTP is the language your client and server speak to each other. If you don't fully understand HTTP, you will:
- Choose the wrong status code and confuse your frontend team
- Miss important headers and create security holes
- Design APIs that break under edge cases
- Fail interview questions that test backend fundamentals
This chapter will give you complete command over HTTP. By the end, you'll know every method, every major status code, and exactly what headers do—not from memorization, but from understanding the why behind each one.
Part 1: What is HTTP, Really?
HTTP stands for HyperText Transfer Protocol. Let's break that down:
- Protocol: A set of rules for communication. Just like how two people on a phone call follow rules (say hello, wait for reply, say goodbye), HTTP defines rules for how a client and server exchange messages.
- Transfer: We are transferring data over a network.
- HyperText: Originally designed for HTML documents, but today HTTP carries JSON, images, videos, files—anything.
The Anatomy of an HTTP Request
When your browser (or curl, or Postman) sends a request, it follows an exact format:
GET /api/users/123 HTTP/1.1 ← Request Line (Method + Path + Version)
Host: api.example.com ← Header
Accept: application/json ← Header
Authorization: Bearer eyJhbGciOi... ← Header
← Empty line (marks end of headers)
← Body (empty for GET requests)
There are three parts:
- Request Line: The first line. Contains the HTTP method (
GET), the path (/api/users/123), and the HTTP version (HTTP/1.1). - Headers: Key-value pairs that provide metadata about the request. Each header is on its own line.
- Body: Optional data sent along with the request (used with
POST,PUT,PATCH). Separated from headers by a blank line.
The Anatomy of an HTTP Response
The server replies with a similar structure:
HTTP/1.1 200 OK ← Status Line (Version + Code + Reason)
Content-Type: application/json ← Header
Content-Length: 82 ← Header
X-Response-Time: 14ms ← Header
← Empty line
{"success": true, "data": {"id": 123}} ← Body
Three parts again:
- Status Line: HTTP version, a numeric status code (
200), and a human-readable reason phrase (OK). - Headers: Metadata about the response.
- Body: The actual data being sent back.
Part 2: HTTP Methods — What Operation Does the Client Want?
An HTTP method (also called a "verb") tells the server what action the client wants to perform.
The 5 Core Methods You'll Use Daily
GET — "Show me this resource"
app.get("/api/users", (req, res) => {
// Fetch and return users
});
Rules of GET:
- Read-only. A GET request should never create, update, or delete anything.
- No body. GET requests don't have a request body. All parameters go in the URL or query string:
/api/users?role=admin&page=2. - Safe. Calling it 100 times should produce the same result as calling it once (no side effects).
- Cacheable. Browsers and CDNs can cache GET responses.
POST — "Create a new resource"
app.post("/api/users", (req, res) => {
// Create a new user from req.body
});
Rules of POST:
- Creates something new. A POST to
/api/userscreates a new user. - Has a body. The request body contains the data for the new resource.
- Not idempotent. If you send the same POST request 3 times, you might get 3 new users. (We'll define "idempotent" in a moment.)
PUT — "Replace this entire resource"
app.put("/api/users/:id", (req, res) => {
// Replace the entire user document with req.body
});
Rules of PUT:
- Full replacement. You must send the complete updated resource in the body. If you omit a field, it should be removed or set to its default.
- Idempotent. Sending the same PUT request 10 times produces the same result as sending it once—the resource ends up in the same state.
PATCH — "Update part of this resource"
app.patch("/api/users/:id", (req, res) => {
// Update only the fields included in req.body
});
Rules of PATCH:
- Partial update. You only send the fields you want to change:
{ "name": "New Name" }. All other fields stay untouched. - Technically not guaranteed idempotent (though in practice, most implementations are).
DELETE — "Remove this resource"
app.delete("/api/users/:id", (req, res) => {
// Delete the user with this ID
});
Rules of DELETE:
- Removes the resource. After a successful DELETE, a subsequent GET to the same URL should return 404.
- Idempotent. Deleting something that's already deleted? No problem—the end state is the same (it doesn't exist).
PUT vs. PATCH — The Most Common Interview Question
This trips up so many developers. Let's use a concrete example.
You have a user in the database:
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
}
PUT /api/users/123 with this body:
{
"name": "Alice Updated",
"email": "alice@example.com"
}
Result: Since PUT is a full replacement, the role field is gone:
{
"id": 123,
"name": "Alice Updated",
"email": "alice@example.com"
// role is GONE because you didn't include it!
}
PATCH /api/users/123 with this body:
{
"name": "Alice Updated"
}
Result: Only name changes. Everything else stays:
{
"id": 123,
"name": "Alice Updated",
"email": "alice@example.com",
"role": "admin"
}
Rule of thumb: Use PATCH for most updates in real applications. Use PUT only when the client is sending a complete, fully-formed resource.
What Does "Idempotent" Mean?
A request is idempotent if making it once has the same effect as making it multiple times.
| Method | Idempotent? | Why? |
|---|---|---|
GET |
✅ Yes | Reading data doesn't change anything |
PUT |
✅ Yes | Replacing with the same data → same result |
DELETE |
✅ Yes | Deleting already-deleted item → still deleted |
PATCH |
⚠️ Depends | Usually yes, but technically not guaranteed |
POST |
❌ No | Creating a new resource each time → different result |
Why does this matter? Because in unreliable networks, requests can be accidentally sent twice (the user double-clicked, the network retried). With idempotent methods, duplicates are harmless. With POST, you might accidentally create two orders.
Two Lesser-Used Methods
OPTIONS — "What methods do you support?"
The browser sends this automatically before cross-origin requests (CORS preflight):
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
The server replies with which methods and headers are allowed. Your cors() middleware handles this automatically.
HEAD — "Same as GET, but only send headers"
HEAD /api/users/123 HTTP/1.1
The server responds with the same status code and headers as a GET, but no body. This is useful for checking if a resource exists or getting its size (Content-Length) without downloading the actual data.
Part 3: HTTP Status Codes — The Server's Answer Code
Every HTTP response includes a 3-digit status code. The first digit tells you the category:
1xx → Informational (rare, you'll rarely use these)
2xx → Success! Everything went well.
3xx → Redirect. The resource moved somewhere else.
4xx → Client Error. The client did something wrong.
5xx → Server Error. The server broke.
The Status Codes You Must Know
2xx — Success
| Code | Name | When to Use | Express Example |
|---|---|---|---|
200 |
OK | General success for GET, PUT, PATCH, DELETE | res.status(200).json(data) |
201 |
Created | A new resource was successfully created (POST) | res.status(201).json(newUser) |
204 |
No Content | Success, but nothing to send back (DELETE) | res.status(204).send() |
When to use 200 vs 201: Use 201 specifically when a POST creates a new record. Use 200 for everything else that succeeds.
When to use 204: After a DELETE, you've removed the resource. There's nothing meaningful to return in the body, so you send 204 No Content.
3xx — Redirection
| Code | Name | When to Use |
|---|---|---|
301 |
Moved Permanently | The URL has permanently changed. Browsers cache this. |
302 |
Found (Temporary Redirect) | Temporary redirect. Don't cache. |
304 |
Not Modified | The client's cached version is still valid (used with ETag/If-None-Match). |
304 is important for performance: When a browser requests a resource it already has cached, the server can check if the resource changed. If not, it sends 304 with no body—saving bandwidth and time.
4xx — Client Errors
| Code | Name | When to Use | Example Scenario |
|---|---|---|---|
400 |
Bad Request | The request body is malformed or invalid | Sending { "email": "not-an-email" } |
401 |
Unauthorized | Authentication required but missing or invalid | No JWT token in the header |
403 |
Forbidden | Authenticated but not authorized | A regular user trying to access admin routes |
404 |
Not Found | The requested resource doesn't exist | GET /api/users/999 when user 999 doesn't exist |
409 |
Conflict | The request conflicts with current state | Trying to create a user with an email that already exists |
422 |
Unprocessable Entity | The syntax is valid but the data is semantically wrong | A date field has "2025-13-45" (valid JSON, impossible date) |
429 |
Too Many Requests | Rate limit exceeded | Client sent 100 requests in 10 seconds |
The 401 vs 403 confusion—solved forever:
401= "Who are you? Show me your ID." (Authentication problem)403= "I know who you are, but you're not allowed in here." (Authorization problem)
Think of it like a nightclub:
401: You showed up without an ID. The bouncer says: "Come back with an ID."403: You showed your ID, but you're 16 and the club is 18+. The bouncer says: "I know who you are. You can't come in."
5xx — Server Errors
| Code | Name | When to Use |
|---|---|---|
500 |
Internal Server Error | Something unexpected broke on the server (unhandled exception, null pointer) |
502 |
Bad Gateway | Your server tried to call another service (database, API) and that service failed |
503 |
Service Unavailable | The server is temporarily down (maintenance, overloaded) |
Rule of thumb: If the client can fix the problem by changing their request, use 4xx. If the problem is on your end and the client can't do anything about it, use 5xx.
Part 4: HTTP Headers — Metadata That Powers Everything
Headers are key-value pairs that carry important metadata. Think of them like the envelope of a letter—the letter itself is the body, but the envelope tells you the sender, recipient, and special handling instructions.
Essential Request Headers (Client → Server)
Content-Type
Tells the server what format the request body is in:
Content-Type: application/json ← JSON data
Content-Type: application/x-www-form-urlencoded ← HTML form data
Content-Type: multipart/form-data ← File uploads
Content-Type: text/plain ← Plain text
This is how express.json() decides whether to parse the body. It checks this header first!
Accept
Tells the server what format the client wants the response in:
Accept: application/json ← "Please give me JSON"
Accept: text/html ← "Please give me HTML"
Accept: */* ← "I'll take anything"
This is called content negotiation—the client and server agree on a data format.
Authorization
Carries authentication credentials:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ← JWT token
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= ← Base64-encoded username:password
User-Agent
Identifies what software is making the request:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64) Chrome/120.0.0.0
User-Agent: PostmanRuntime/7.36.0
User-Agent: axios/1.6.2
Useful for logging and analytics—you can see what percentage of your traffic comes from mobile vs. desktop.
Cookie
Sends stored cookies back to the server:
Cookie: sessionId=abc123; theme=dark
Essential Response Headers (Server → Client)
Content-Type
Tells the client what format the response body is in:
Content-Type: application/json; charset=utf-8
When you call res.json() in Express, it automatically sets this header for you.
Content-Length
The size of the response body in bytes:
Content-Length: 1482
Helps the client know when the complete response has been received.
Set-Cookie
Tells the browser to store a cookie:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400
HttpOnly: JavaScript can't access this cookie (protects against XSS).Secure: Only sent over HTTPS.SameSite=Strict: Not sent with cross-origin requests (protects against CSRF).Max-Age=86400: Expires after 24 hours (86400 seconds).
Cache-Control
Tells browsers and CDNs how long to cache the response:
Cache-Control: public, max-age=3600 ← Cache for 1 hour
Cache-Control: private, no-cache ← Don't cache (for personal data)
Cache-Control: no-store ← Never store anywhere (sensitive data)
ETag (Entity Tag)
A unique fingerprint of the response content:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Here's how ETag caching works:
1. First request:
Client: GET /api/products/1
Server: 200 OK + ETag: "abc123" + full body
2. Second request:
Client: GET /api/products/1 + If-None-Match: "abc123"
Server checks: Has product 1 changed? No.
Server: 304 Not Modified (no body! saves bandwidth!)
3. Third request (after product was updated):
Client: GET /api/products/1 + If-None-Match: "abc123"
Server checks: Has product 1 changed? Yes!
Server: 200 OK + ETag: "def456" + full body
Location
Used with 201 and 3xx status codes. Tells the client where the new or moved resource lives:
HTTP/1.1 201 Created
Location: /api/users/456
Part 5: The Complete Request-Response Lifecycle in Express
Let's see how all of this comes together in a real Express route. Here's a POST request to create a user:
The Raw HTTP Request:
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
Accept: application/json
Content-Length: 52
{"name": "Bob", "email": "bob@example.com"}
The Express Handler:
export async function createUser(req, res, next) {
try {
// express.json() already parsed the body for us
// because Content-Type was application/json
const { name, email } = req.body;
// Authorization header is available on req.headers
const token = req.headers.authorization; // "Bearer eyJhbGci..."
const newUser = await userService.createUser({ name, email });
// 201 Created + Location header pointing to the new resource
return res
.status(201)
.location(`/api/users/${newUser._id}`)
.json({
success: true,
data: newUser,
});
} catch (error) {
next(error);
}
}
The Raw HTTP Response:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/users/65bb0a38e8202d6b1d1e44a2
Content-Length: 127
{"success":true,"data":{"_id":"65bb0a38e8202d6b1d1e44a2","name":"Bob","email":"bob@example.com","role":"member"}}
Notice how every concept we discussed plays a role:
- The method (
POST) told Express which route handler to use. - The Content-Type header told
express.json()to parse the body. - The status code (
201) told the client the resource was created. - The Location header told the client where to find the new resource.
- The Content-Type response header told the client the response is JSON.
Part 6: HTTP Versions — A Brief Overview
HTTP/1.1 (What you're using right now)
- Text-based protocol. Headers are sent as plain text.
- One request at a time per connection. The browser can keep the TCP connection alive (
Keep-Alive), but requests are processed sequentially. - Head-of-line blocking. If request 1 is slow, requests 2 and 3 have to wait.
- Browsers work around this by opening 6 parallel TCP connections to the same server.
HTTP/2 (Used by most production websites)
- Binary protocol. More efficient than text.
- Multiplexing. Multiple requests can be sent simultaneously on a single TCP connection, interleaved.
- Header compression (HPACK). Headers are compressed, saving bandwidth on repetitive headers.
- Server Push. The server can proactively send resources before the client asks.
- Fully backward-compatible with HTTP/1.1 at the application level. Your Express code works identically.
HTTP/3 (Emerging)
- Replaces TCP with QUIC (a UDP-based protocol developed by Google).
- Eliminates TCP-level head-of-line blocking.
- Built-in encryption (TLS 1.3 is mandatory).
- Faster connection establishment.
For your Express applications, HTTP versions are mostly handled by your reverse proxy (Nginx, Cloudflare). Your application code remains the same regardless of the version—the methods, status codes, and headers we learned are universal across all versions.
Part 7: Common Mistakes and How to Avoid Them
Mistake 1: Using 200 for everything
// ❌ Don't do this
app.post("/api/users", async (req, res) => {
const user = await createUser(req.body);
res.status(200).json(user); // Should be 201!
});
app.delete("/api/users/:id", async (req, res) => {
await deleteUser(req.params.id);
res.status(200).json({ message: "Deleted" }); // Should be 204!
});
// ✅ Do this
app.post("/api/users", async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user); // 201 = Created
});
app.delete("/api/users/:id", async (req, res) => {
await deleteUser(req.params.id);
res.status(204).send(); // 204 = No Content
});
Mistake 2: Using GET for actions that change data
// ❌ Terrible: GET should never modify data
app.get("/api/users/:id/delete", async (req, res) => {
await deleteUser(req.params.id);
res.json({ deleted: true });
});
// ❌ Terrible: GET should never create data
app.get("/api/send-email?to=bob@mail.com", async (req, res) => {
await sendEmail(req.query.to);
res.json({ sent: true });
});
Why is this dangerous? Because:
- Search engine crawlers follow GET links and would accidentally delete your users.
- Browsers pre-fetch GET URLs and could trigger unwanted actions.
- Caches might serve the response without ever hitting your server again.
Mistake 3: Confusing 401 and 403
// ❌ Wrong: this should be 401
if (!token) {
return res.status(403).json({ error: "Not authorized" });
}
// ✅ Correct
if (!token) {
return res.status(401).json({ error: "Authentication required" }); // No token = who are you?
}
if (user.role !== "admin") {
return res.status(403).json({ error: "Admin access required" }); // I know you, but you can't
}
Mistake 4: Not setting Content-Type
// ❌ Client has no idea what format the response is
res.send('{"user": "Alice"}');
// ✅ Express's res.json() automatically sets Content-Type
res.json({ user: "Alice" });
// Sets: Content-Type: application/json; charset=utf-8
Quick Reference Card
Methods
| Method | Purpose | Has Body? | Idempotent? |
|---|---|---|---|
GET |
Read a resource | ❌ No | ✅ Yes |
POST |
Create a resource | ✅ Yes | ❌ No |
PUT |
Replace a resource entirely | ✅ Yes | ✅ Yes |
PATCH |
Update parts of a resource | ✅ Yes | ⚠️ Usually |
DELETE |
Remove a resource | ❌ Usually no | ✅ Yes |
OPTIONS |
Check allowed methods (CORS preflight) | ❌ No | ✅ Yes |
HEAD |
Same as GET but no body in response | ❌ No | ✅ Yes |
Status Codes
| Code | Name | TL;DR |
|---|---|---|
200 |
OK | It worked |
201 |
Created | New thing created |
204 |
No Content | It worked, nothing to return |
301 |
Moved Permanently | URL changed forever |
304 |
Not Modified | Your cache is still good |
400 |
Bad Request | Your input is broken |
401 |
Unauthorized | Who are you? Login first |
403 |
Forbidden | I know you, but no access |
404 |
Not Found | That thing doesn't exist |
409 |
Conflict | Clashes with existing data |
422 |
Unprocessable Entity | Valid format, impossible data |
429 |
Too Many Requests | Slow down |
500 |
Internal Server Error | Server broke |
502 |
Bad Gateway | Upstream service failed |
503 |
Service Unavailable | Temporarily down |
Summary & What Comes Next
We've now mastered the language your client and server speak:
- HTTP requests have three parts: request line (method + path), headers, and body.
- HTTP methods define the operation: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes.
- Idempotency determines if repeating a request is safe—crucial for unreliable networks.
- Status codes communicate the result: 2xx success, 3xx redirect, 4xx client error, 5xx server error.
- Headers carry essential metadata:
Content-Type,Authorization,Cookie,Cache-Control,ETag. - HTTP versions evolve the transport (text → binary → QUIC) but the application-level concepts stay the same.
In the next chapter, we will apply these concepts to design clean, professional REST APIs—covering URL naming conventions, pagination, filtering, sorting, versioning, and error response formats.