Explorer
Node.js

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:

TEXT
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:

  1. Request Line: The first line. Contains the HTTP method (GET), the path (/api/users/123), and the HTTP version (HTTP/1.1).
  2. Headers: Key-value pairs that provide metadata about the request. Each header is on its own line.
  3. 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:

TEXT
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:

  1. Status Line: HTTP version, a numeric status code (200), and a human-readable reason phrase (OK).
  2. Headers: Metadata about the response.
  3. 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"

JAVASCRIPT
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"

JAVASCRIPT
app.post("/api/users", (req, res) => {
  // Create a new user from req.body
});

Rules of POST:

  • Creates something new. A POST to /api/users creates 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"

JAVASCRIPT
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"

JAVASCRIPT
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"

JAVASCRIPT
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:

JSON
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "admin"
}

PUT /api/users/123 with this body:

JSON
{
  "name": "Alice Updated",
  "email": "alice@example.com"
}

Result: Since PUT is a full replacement, the role field is gone:

JSON
{
  "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:

JSON
{
  "name": "Alice Updated"
}

Result: Only name changes. Everything else stays:

JSON
{
  "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):

TEXT
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"

TEXT
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:

TEXT
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:

TEXT
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:

TEXT
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:

TEXT
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...   ← JWT token
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=   ← Base64-encoded username:password

User-Agent

Identifies what software is making the request:

TEXT
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:

TEXT
Cookie: sessionId=abc123; theme=dark

Essential Response Headers (Server → Client)

Content-Type

Tells the client what format the response body is in:

TEXT
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:

TEXT
Content-Length: 1482

Helps the client know when the complete response has been received.

Set-Cookie

Tells the browser to store a cookie:

TEXT
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:

TEXT
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:

TEXT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Here's how ETag caching works:

TEXT
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:

TEXT
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:

TEXT
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:

JAVASCRIPT
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:

TEXT
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

JAVASCRIPT
// ❌ 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!
});
JAVASCRIPT
// ✅ 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

JAVASCRIPT
// ❌ 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

JAVASCRIPT
// ❌ 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

JAVASCRIPT
// ❌ 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:

  1. HTTP requests have three parts: request line (method + path), headers, and body.
  2. HTTP methods define the operation: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes.
  3. Idempotency determines if repeating a request is safe—crucial for unreliable networks.
  4. Status codes communicate the result: 2xx success, 3xx redirect, 4xx client error, 5xx server error.
  5. Headers carry essential metadata: Content-Type, Authorization, Cookie, Cache-Control, ETag.
  6. 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.

Finished this lesson?

Mark this chapter complete to update your learning streak and unlock the next lesson.