The Anatomy of an HTTP Request
When your browser wants a web page, it sends an HTTP Request. But what's actually inside that request? It's not just "Give me the page!" — it's a carefully structured message that follows a precise set of rules defined in the HTTP specification.
1. The Four Parts of Every HTTP Request
┌─────────────────────────────────────────────────────────┐
│ REQUEST LINE │
│ POST /api/users HTTP/1.1 │
├─────────────────────────────────────────────────────────┤
│ HEADERS │
│ Host: api.nandhoo.com │
│ Content-Type: application/json │
│ Authorization: Bearer eyJhbGci... │
│ Accept: application/json │
│ User-Agent: Mozilla/5.0 (Macintosh; Intel...) │
├─────────────────────────────────────────────────────────┤
│ BLANK LINE (separates headers from body) │
│ │
├─────────────────────────────────────────────────────────┤
│ BODY (optional — only for POST, PUT, PATCH) │
│ {"username": "Nandhoo", "email": "hi@nandhoo.com"} │
└─────────────────────────────────────────────────────────┘
2. The Request Line
The first line of every HTTP request contains three things:
METHOD PATH HTTP-VERSION
│ │ │
GET /api/users HTTP/1.1
Method
The verb that describes the action you want to perform. Common methods:
| Method | Purpose | Has Body? | Idempotent? |
|---|---|---|---|
GET | Retrieve a resource | No | Yes |
POST | Create a new resource | Yes | No |
PUT | Replace an existing resource entirely | Yes | Yes |
PATCH | Update part of a resource | Yes | No |
DELETE | Remove a resource | Optional | Yes |
HEAD | Same as GET but returns headers only | No | Yes |
OPTIONS | Ask which methods a server supports | No | Yes |
Idempotent means calling the method multiple times has the same effect as calling it once. DELETE /users/123 twice still results in the user being deleted — same end state.
Path
The specific resource you want to access on the server.
/api/users/42/posts?page=2&limit=10
│ │ │
│ │ └── Query string (extra parameters)
│ └──────── Resource identifier
└─────────────────────── Base path
HTTP Version
| Version | Year | Key feature |
|---|---|---|
| HTTP/1.0 | 1996 | New connection for every request |
| HTTP/1.1 | 1997 | Persistent connections (keep-alive), chunked transfer |
| HTTP/2 | 2015 | Multiplexing, header compression, server push |
| HTTP/3 | 2022 | Built on QUIC/UDP, even faster |
3. The URL and its Components
A full URL has several distinct parts:
https://api.nandhoo.com:443/v1/users/42?tab=badges&sort=asc#section-1
│ │ │ │ │ │
│ │ │ │ │ └── Fragment (not sent to server)
│ │ │ │ └────────────────────── Query string
│ │ │ └────────────────────────────────── Path
│ │ └────────────────────────────────────── Port (optional if default)
│ └────────────────────────────────────────────────── Host (domain)
└────────────────────────────────────────────────────────────── Scheme / Protocol
Query Strings
Query strings pass extra parameters to the server without changing the path:
/api/users?sort=desc&page=3&limit=20&search=alice
Key-value pairs:
sort = desc
page = 3
limit = 20
search= alice
In JavaScript, use URLSearchParams to safely build query strings:
const params = new URLSearchParams({
sort: 'desc',
page: '3',
limit: '20',
search: 'alice'
});
const url = `https://api.nandhoo.com/users?${params}`;
// → https://api.nandhoo.com/users?sort=desc&page=3&limit=20&search=alice
fetch(url).then(r => r.json()).then(console.log);
URL Encoding
Special characters in URLs must be percent-encoded:
| Character | Encoded |
|---|---|
| Space | %20 or + |
& | %26 |
= | %3D |
# | %23 |
/ | %2F |
@ | %40 |
encodeURIComponent() handles this for you:
const search = 'coffee & cake';
const safe = encodeURIComponent(search); // 'coffee%20%26%20cake'
4. Request Headers — The Metadata Envelope
Headers give the server extra context. Each is a Key: Value pair on its own line.
Common Request Headers
| Header | Example | Purpose |
|---|---|---|
Host | api.nandhoo.com | Which server to talk to (required in HTTP/1.1+) |
User-Agent | Mozilla/5.0 (Mac) | Browser and OS info |
Accept | application/json | What response format the client wants |
Accept-Language | en-GB,en;q=0.9 | Preferred language(s) |
Accept-Encoding | gzip, deflate, br | Compression algorithms the client supports |
Content-Type | application/json | Format of the request body |
Content-Length | 47 | Size of the request body in bytes |
Authorization | Bearer eyJhbGci... | Authentication token or credentials |
Cookie | sessionId=abc123 | Cookies to send with the request |
Referer | https://google.com | URL of the previous page |
Origin | https://nandhoo.com | Where the request originated (CORS) |
Cache-Control | no-cache | Caching directives |
Connection | keep-alive | Keep the TCP connection open for reuse |
The Accept Header and Content Negotiation
The Accept header lets the client tell the server what format it can handle. The server picks the best match.
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Meaning:
text/html → quality 1.0 (most preferred)
application/xhtml+xml → quality 1.0
application/xml → quality 0.9
*/* → quality 0.8 (accept anything as last resort)
5. The Request Body
The body carries the actual data for POST, PUT, and PATCH requests. GET and DELETE requests rarely have a body.
Body Format 1: JSON (Most Common for APIs)
fetch('https://api.nandhoo.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'Nandhoo',
email: 'hi@nandhoo.com',
level: 'beginner'
})
});
Raw request on the wire:
POST /users HTTP/1.1
Host: api.nandhoo.com
Content-Type: application/json
Content-Length: 62
{"username":"Nandhoo","email":"hi@nandhoo.com","level":"beginner"}
Body Format 2: Form Data (HTML Form Submissions)
<form method="POST" action="/login">
<input name="email" type="email">
<input name="password" type="password">
<button type="submit">Log in</button>
</form>
The browser sends:
POST /login HTTP/1.1
Host: www.nandhoo.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 41
email=hi%40nandhoo.com&password=secret123
Body Format 3: Multipart Form Data (File Uploads)
<form method="POST" action="/upload" enctype="multipart/form-data">
<input name="avatar" type="file">
<button type="submit">Upload</button>
</form>
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg
[binary JPEG data here...]
------WebKitFormBoundaryABC123--
Body Format 4: Plain Text
fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'This is my plain text note.'
});
6. A Complete Real HTTP Request
Here is an actual raw request as it travels across the network:
GET /api/v1/courses?level=beginner HTTP/1.1
Host: api.nandhoo.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept: application/json
Accept-Language: en-GB,en;q=0.9
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.X5k
Connection: keep-alive
Notice there's no body — this is a GET request.
7. CORS — Cross-Origin Requests
When your JavaScript fetches a URL on a different origin (different domain, protocol, or port), the browser adds an Origin header and the server must respond with permission.
// Your page is on: https://nandhoo.com
// This request goes to a DIFFERENT origin:
fetch('https://api.otherdomain.com/data');
// Browser adds: Origin: https://nandhoo.com
The server must include Access-Control-Allow-Origin: https://nandhoo.com (or *) in its response, otherwise the browser blocks the response entirely.
Preflight Requests
For non-simple requests (e.g., PUT with a custom header), the browser sends a preflight request first:
OPTIONS /api/resource HTTP/1.1
Host: api.otherdomain.com
Origin: https://nandhoo.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type
If the server responds with appropriate Access-Control-Allow-* headers, the actual request proceeds.
8. Building Requests with fetch — Full Reference
const response = await fetch('https://api.nandhoo.com/courses', {
method: 'POST', // HTTP method
headers: { // Request headers
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
'Accept': 'application/json',
'X-App-Version': '1.2.0' // Custom headers start with X-
},
body: JSON.stringify({ // Request body (auto-serialised)
title: 'Intro to HTTP',
level: 'beginner',
chapters: 10
}),
credentials: 'include', // Send cookies cross-origin
mode: 'cors', // CORS mode
cache: 'no-cache', // Don't use cached response
signal: AbortSignal.timeout(5000) // Cancel after 5 seconds
});
9. Practical Patterns
Pattern 1 — GET with Query Parameters
async function searchCourses(query, level = 'all', page = 1) {
const params = new URLSearchParams({ q: query, level, page });
const res = await fetch(`/api/courses?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const results = await searchCourses('javascript', 'beginner', 2);
Pattern 2 — POST with Error Handling
async function createUser(userData) {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (res.status === 409) throw new Error('Username already taken');
if (res.status === 422) {
const errors = await res.json();
throw new Error('Validation failed: ' + JSON.stringify(errors));
}
if (!res.ok) throw new Error(`Unexpected error: ${res.status}`);
return res.json(); // 201 Created with new user object
}
Pattern 3 — File Upload with Progress
async function uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file);
formData.append('description', 'Profile photo');
// Note: DO NOT set Content-Type header manually —
// the browser adds the correct multipart boundary automatically
const res = await fetch('/api/upload/avatar', {
method: 'POST',
body: formData
});
return res.json();
}
10. Common Mistakes
| Mistake | What goes wrong | Fix |
|---|---|---|
Forgetting Content-Type: application/json on a POST | Server receives garbled data or ignores the body | Always set Content-Type when sending JSON |
Setting Content-Type: multipart/form-data manually | Browser can't add the required boundary parameter | Never set Content-Type for FormData uploads — let the browser do it |
| Putting sensitive data in the URL query string | URLs appear in server logs, browser history, and the Referer header | Use the request body or Authorization header for secrets |
| Not handling non-2xx responses | .json() on an error response crashes or shows confusing data | Always check response.ok or response.status before parsing |
| Building query strings with string concatenation | Special characters break the URL | Use URLSearchParams |
11. Review Questions
- What are the four parts of an HTTP request?
- What does
Content-Type: application/jsontell the server? - What does an
Authorization: Bearerheader contain? - When should you use a request body, and for which HTTP methods?
- Why should you never put a password in a query string?
- What is URL encoding and why is it needed?
- What happens in a CORS preflight OPTIONS request?