How HTTP works and what an HTTP request consists of
HTTP (HyperText Transfer Protocol) is a stateless application-layer protocol where a client sends a structured text request to a server, and the server sends back a response with a status code, headers, and an optional body.
Theory
TL;DR
- HTTP is like mailing a letter: you write method + path + headers + body, drop it at a TCP port, and get a reply with a status code
- A request has five parts: method, URL, HTTP version, headers, body
- Stateless means the server forgets your previous request the moment it responds
- GET for reading, POST for creating, PUT/PATCH for updating, DELETE for removing
- Use HTTP/2 when you need many parallel requests (it multiplexes streams over one TCP connection)
Quick example
// This is what happens when you call fetch()
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(res => {
console.log('Status:', res.status); // 200
console.log('Content-Type:', res.headers.get('content-type')); // application/json
return res.json();
})
.then(data => console.log('Name:', data.name)); // Leanne Graham
// Under the hood, the browser sends:
// GET /users/1 HTTP/1.1
// Host: jsonplaceholder.typicode.com
// Accept: application/jsonWhen you call fetch(), the browser serializes those five parts into bytes, opens a TCP connection to port 443 (HTTPS), and writes them to the socket. The server reads the stream, parses it, and writes a response back.
What an HTTP request actually looks like
An HTTP/1.1 request is plain text. Here is a raw POST request:
POST /todos HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGc...
Content-Length: 32
{"text": "Buy milk", "done": false}Four sections, one blank line separating headers from body. That is the whole format.
Method tells the server what to do with the resource. GET reads, POST creates, PUT replaces, PATCH updates partially, DELETE removes. The method also signals caching behavior: GET responses can be cached, POST ones cannot.
URL is the resource address. It includes the path (/todos) and optionally query params (?page=1&sort=asc).
Headers are key-value metadata. Content-Type: application/json tells the server how to parse the body. Authorization: Bearer token authenticates the request. Host is required in HTTP/1.1 because one IP can serve many domains.
Body carries the payload. Only POST, PUT, and PATCH use it. GET and DELETE should not have a body (more on that in Common mistakes).
How the request travels
The browser resolves the domain via DNS, opens a TCP connection to port 80 (HTTP) or 443 (HTTPS), and writes the request bytes into the kernel buffer. For HTTPS, a TLS handshake comes first: the client sends a hello, the server responds with its certificate, they negotiate a cipher, and from that point all bytes are encrypted. The HTTP structure inside stays the same.
On the server side, a library like http-parser (a C library used in both Node.js and nginx) reads the incoming stream and fills in the method, path, headers, and body. Then your application code takes over.
HTTP/1.1 vs HTTP/2
HTTP/1.1 is text-based. You get one request at a time per TCP connection. If you send request A then request B on the same connection, B waits for A to finish. Browsers worked around this by opening six connections per domain, but that is still wasteful.
HTTP/2 replaces the text format with binary frames and multiplexes multiple request streams over one TCP connection. A slow request does not block others. Same HTTP methods and headers, different wire format.
HTTP/3 (QUIC) goes further: it drops TCP entirely in favor of UDP with per-stream loss recovery. A dropped packet stalls only its own stream, not the whole connection.
Common mistakes
Sending a body with GET. RFC 7231 does not forbid it, but proxies and caches may drop or ignore the body. You get zero bytes on the server and confused caching logs. In production, the CDN is usually what exposes this. The origin server processes the request fine, but after you add CloudFront or Cloudflare, the cached response starts coming back wrong and nobody connects it to the GET body.
// Wrong: body gets ignored by most servers and proxies
fetch('/search', { method: 'GET', body: 'q=shoes' });
// Right: use query params
fetch('/search?q=shoes');Assuming the server remembers you. HTTP is stateless. Every request is a blank slate. A POST adds an item to a cart; the next request arrives and the server has no idea who you are.
// Wrong: no session middleware means req.session is undefined
app.post('/cart', (req, res) => {
req.session.cart.push(req.body.item); // TypeError: Cannot read property 'cart' of undefined
});
// Right: set up session middleware first
app.use(session({ secret: 'key', resave: false, saveUninitialized: true }));Missing Content-Type on POST. If you send a JSON body without Content-Type: application/json, Express (and most frameworks) will not know how to parse it. req.body ends up undefined.
// Wrong: server receives body as raw stream, req.body is undefined
fetch('/todos', {
method: 'POST',
body: JSON.stringify({ text: 'Buy milk' }) // No Content-Type header
});
// Right
fetch('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'Buy milk' })
});Skipping certificate validation in Node.js. Setting rejectUnauthorized: false in development feels harmless, but the habit spreads to production. A misconfigured server becomes a man-in-the-middle attack waiting to happen.
Real-world usage
- React:
useEffect+fetch('/api/users')sends a GET and parses the JSON response into component state - Express:
app.get('/profile', (req, res) => res.json(user))responds with 200 + Content-Type: application/json - Next.js API routes:
export default (req, res) => res.status(201).json(data)parses the full request including method and headers - Apollo Client: sends GraphQL queries as POST with an
Authorizationheader and a JSON body - AWS Lambda + API Gateway: receives method, path, headers, and body as a parsed event object
Follow-up questions
Q: What is the difference between HTTP/1.1 and HTTP/2?
A: HTTP/1.1 is text-based and handles one request per TCP connection at a time, causing head-of-line blocking. HTTP/2 uses binary frames and multiplexes many streams over one connection, so a slow request does not stall others.
Q: What is in the request line vs headers?
A: The request line is the first line: METHOD /path HTTP/1.1. Headers follow, one per line in Name: Value format, ending with a blank line before the body.
Q: How does HTTPS change the request?
A: It wraps the request in TLS. A handshake negotiates encryption keys, then all bytes including headers and body travel encrypted. The HTTP format inside is unchanged.
Q: What do status codes 200, 201, and 204 mean?
A: 200 OK with a body. 201 Created, usually returned after POST with a Location header pointing to the new resource. 204 No Content, typically after a successful DELETE.
Q: What triggers a CORS preflight request?
A: The browser sends an OPTIONS request first when the actual request is cross-origin and uses a non-simple method or a non-simple header like Authorization. The server must reply with Access-Control-Allow-Origin before the browser sends the real request.
Q: How does HTTP/3 (QUIC) fix the head-of-line blocking that HTTP/2 still has?
A: HTTP/2 multiplexes streams over one TCP connection, but TCP itself blocks all streams when a single packet is lost. QUIC runs over UDP and implements per-stream loss recovery, so a dropped packet stalls only its own stream.
Examples
Basic: reading request parts in Express
const express = require('express');
const app = express();
app.use(express.json()); // parses body when Content-Type is application/json
app.post('/todos', (req, res) => {
console.log('Method:', req.method); // POST
console.log('Path:', req.path); // /todos
console.log('Auth:', req.headers.authorization); // Bearer xyz
console.log('Body:', req.body); // { text: 'Buy milk', done: false }
res.status(201).json({ id: 1, ...req.body });
// Response: HTTP/1.1 201 Created
// Content-Type: application/json
// { "id": 1, "text": "Buy milk", "done": false }
});
app.listen(3000);
// Test with curl:
// curl -X POST http://localhost:3000/todos \
// -H "Content-Type: application/json" \
// -H "Authorization: Bearer xyz" \
// -d '{"text":"Buy milk","done":false}'Express unpacks all five parts of the request for you. The express.json() middleware reads the body stream, checks Content-Type, and parses the JSON into req.body. Without it, req.body is undefined.
Intermediate: head-of-line blocking in practice
const https = require('https');
// Two requests on separate connections - both start immediately
const req1 = https.request('https://httpbin.org/get', { method: 'GET' }, res1 => {
console.log('Req1 status:', res1.statusCode); // 200, arrives fast
});
const req2 = https.request('https://httpbin.org/delay/2', { method: 'GET' }, res2 => {
console.log('Req2 status:', res2.statusCode); // 200, arrives after 2s
});
req1.end();
req2.end();
// Node opens separate TCP connections here, so they run in parallel
// On a single HTTP/1.1 connection, req2 would block req1 the whole 2s
// This is why browsers opened 6 connections per domain in the HTTP/1.1 eraEach https.request() call opens its own TCP connection, so both run in parallel. On a single HTTP/1.1 connection the slow request would stall everything behind it. HTTP/2 fixes this by streaming both over one connection without blocking.
Advanced: inspecting raw request structure with Node's http module
const http = require('http');
http.createServer((req, res) => {
// req.method, req.url, req.httpVersion come from the request line
console.log(`${req.method} ${req.url} HTTP/${req.httpVersion}`);
// GET /api/data HTTP/1.1
// req.headers is an object with all header key-value pairs (lowercased)
console.log('Host:', req.headers.host);
console.log('Accept:', req.headers.accept);
console.log('User-Agent:', req.headers['user-agent']);
// Body must be read as a stream - it is not a string by default
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
console.log('Body:', body); // raw string - use JSON.parse() if needed
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
});
}).listen(3000);Node's http module gives you direct access to all five request parts. The body is a readable stream, not a string. That is exactly why express.json() exists: it handles the streaming and parsing so you get req.body as a plain object instead of wiring up data and end events yourself.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.