Skip to main content

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

javascript
// 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/json

When 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.

javascript
// 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.

javascript
// 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.

javascript
// 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 Authorization header 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

javascript
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

javascript
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 era

Each 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

javascript
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?