
HTTP: The Delivery System of the Web
Request gets Response. But has amnesia (Stateless), can't remember who you were 1 second ago. So we invented cookies as sticky notes.

Request gets Response. But has amnesia (Stateless), can't remember who you were 1 second ago. So we invented cookies as sticky notes.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

I was building my first API. Working on a simple message board backend with Node.js + Express when my teammate asked:
"We should use POST for creating posts, right? But what's actually different from GET?"
I froze. "Um... GET shows in the address bar and POST doesn't..." That's all I had. I didn't know why. I'd just memorized the pattern: "GET for fetching posts, POST for creating them."
Then I hit a bigger wall while building login functionality. Login worked fine, but the next request returned "unauthenticated user." The server seemed to forget the login from 1 second ago. "Does the server have dementia?" I wondered.
That's when I realized: I couldn't build web applications without properly understanding HTTP. So I dove in.
"The server doesn't maintain state"
This made no sense. When you log in, shouldn't the server remember "this is a logged-in user"? But it can't remember? Then how does login persist? I couldn't understand.
Just adding an S to HTTP makes it "secure"? What exactly happens to make it safe?
Then a senior developer gave me this analogy that made everything click:
HTTP is a mailman.
- You: "Deliver this letter" (Request)
- Mailman: "Here you go" (Response)
But this mailman has amnesia. Delivered to you 1 minute ago, but 1 minute later asks "Who are you?"
So you use sticky notes (Cookies): "I'm John. Member ID 12345." Show this note to the mailman → "Oh, John!" (recognized)
Everything made sense in that moment. "That's what it was all along." Stateless wasn't a bug—it was intentional design. Cookies/Sessions were the workaround for this limitation.
The first HTTP Tim Berners-Lee created was truly minimal:
GET /page.html
That's it. Only GET existed. No headers. Only HTML could be transferred. It literally was just a "Protocol to Transfer HyperText."
As the internet grew, HTML alone wasn't enough. We needed to send images, know which browser was requesting. So headers were added:
GET /page.html HTTP/1.0
User-Agent: Mozilla/1.0
Accept: text/html
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1234
<html>...</html>
Now we could specify "what to send (Content-Type)" and "how much (Content-Length)."
HTTP/1.0's problem: it opened a new connection for every request. A webpage with 10 images meant 11 connect-disconnect cycles. Inefficient.
HTTP/1.1 introduced Keep-Alive, allowing connection reuse. The Host header became mandatory, enabling virtual hosting (multiple domains on one IP).
GET / HTTP/1.1
Host: example.com
Connection: keep-alive
This version was the web's standard for 18 years until 2015. Still widely used today.
HTTP/2 (2015) improved speed. Multiple requests could be sent simultaneously over one connection (multiplexing), and headers were compressed.
HTTP/3 (2022) abandoned TCP entirely for QUIC (UDP-based). Even faster.
But the core concepts remain identical to HTTP/1.1: Request-Response, Stateless, header-body structure. The fundamentals haven't changed.
When I first saw an HTTP request, I thought "this looks complicated." But it's actually simple when broken down.
POST /api/posts HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Cookie: session_id=abc123; theme=dark
Content-Length: 45
{"title": "New post", "content": "Hello"}
Four parts:
POST /api/posts HTTP/1.1
Metadata. Headers I commonly use:
Host: Which domain to send to (required)
Host: example.com
User-Agent: Which browser/client
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Server uses this to distinguish mobile/desktop.
Accept: Desired response format
Accept: application/json
"Give me JSON please." Could be text/html, image/png, etc.
Content-Type: Format of data I'm sending
Content-Type: application/json
Required for POST/PUT. Server knows "oh, they sent JSON" and parses accordingly.
Authorization: Auth token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT tokens go here.
Cookie: Cookies
Cookie: session_id=abc123; theme=dark; user_id=456
Browser attaches this automatically. Semicolons separate multiple cookies.
Separates headers from body. Without this, we wouldn't know where headers end.
Actual data. GET/DELETE usually have no body. POST/PUT do.
{"title": "New post", "content": "Hello"}
Server responses have similar structure:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600, public
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Set-Cookie: session_id=xyz789; HttpOnly; Secure; SameSite=Lax
Content-Encoding: gzip
Content-Length: 512
{"id": 123, "title": "New post", "author": "John"}
HTTP/1.1 200 OK
Content-Type: What's being sent
Content-Type: application/json
Cache-Control: Caching policy
Cache-Control: max-age=3600, public
"Cache for 3600 seconds (1 hour). Public caches (like CDNs) OK too"
ETag: Resource version
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content hash. Next request can ask "I have this version, has it changed?"
Set-Cookie: Set a cookie
Set-Cookie: session_id=xyz789; HttpOnly; Secure; SameSite=Lax
Tells browser "save this cookie."
Content-Encoding: Compression method
Content-Encoding: gzip
"Compressed with gzip. Decompress to use"
Same as request.
Initially I only knew GET/POST. Turns out there are more.
GET /posts/123
Characteristics:
GET /search?q=keyword&page=1&limit=10
When to use: Fetch posts, search, view profile
POST /posts
Content-Type: application/json
{"title": "New post", "content": "Content"}
Characteristics:
When to use: Create post, register user, upload file
PUT /posts/123
Content-Type: application/json
{"title": "Updated title", "content": "Updated content"}
Characteristics:
Warning: Sending partial data deletes the rest.
PUT /posts/123
{"title": "Only title"}
This deletes the content field!
PATCH /posts/123
Content-Type: application/json
{"title": "Only title updated"}
Characteristics:
content remains unchangedWhen to use: Update only post title, change only profile picture
DELETE /posts/123
Characteristics:
HEAD /posts/123
Like GET but without body. Only headers returned.
When to use: Check file size only, verify resource exists
// Check file size before download
const response = await fetch('/large-file.zip', { method: 'HEAD' });
const size = response.headers.get('Content-Length');
console.log(`File size: ${size} bytes`);
OPTIONS /posts/123
HTTP/1.1 200 OK
Allow: GET, PUT, PATCH, DELETE
Access-Control-Allow-Methods: GET, PUT, DELETE
When to use: CORS preflight requests automatically use this. Browser asks "can I use this method?"
Idempotent = multiple identical requests produce same result
Why does this matter? It determines whether retrying after network errors is safe.
// DELETE is idempotent, so retry OK
async function deletePost(id) {
let retries = 3;
while (retries > 0) {
try {
await fetch(`/posts/${id}`, { method: 'DELETE' });
break;
} catch (err) {
retries--;
if (retries === 0) throw err;
}
}
}
Initially I only knew "200 is success, 404 is not found, 500 is server dead." Turns out there's much more variety.
100 Continue: "Got request headers, send body now"
For large file uploads, send headers first. If server responds 100, send body. If server refuses, don't send body, saving bandwidth.
200 OK: Success
GET /posts/123
→ 200 OK
201 Created: Created successfully
POST /posts
→ 201 Created
Location: /posts/456
Location header contains new resource URL.
204 No Content: Success but nothing to return
DELETE /posts/123
→ 204 No Content
After successful deletion, nothing to return, right? Use 204.
206 Partial Content: Partial transfer
GET /video.mp4
Range: bytes=0-1023
→ 206 Partial Content
Content-Range: bytes 0-1023/5000000
This is the secret of video streaming. YouTube doesn't download entire videos—it fetches needed chunks. This is how.
301 Moved Permanently: Permanent move
GET /old-page
→ 301 Moved Permanently
Location: /new-page
Search engines learn "oh, this URL changed" and update their index.
302 Found: Temporary move
GET /admin
→ 302 Found
Location: /login
"Go to login page now. Come back to /admin later"
307 Temporary Redirect: Like 302 but preserves method
POST /old-api
→ 307 Temporary Redirect
Location: /new-api
302 may change POST to GET. 307 keeps POST.
308 Permanent Redirect: 301's 307 version
Permanent move that doesn't change method.
400 Bad Request: Malformed request
{"error": "title is required"}
Missing required fields, JSON syntax errors, etc.
401 Unauthorized: Authentication required
GET /api/profile
→ 401 Unauthorized
WWW-Authenticate: Bearer
"Log in first"
403 Forbidden: Insufficient permissions
DELETE /posts/999
→ 403 Forbidden
{"error": "You are not the author"}
Logged in but trying to delete someone else's post.
404 Not Found: Doesn't exist
GET /posts/99999
→ 404 Not Found
429 Too Many Requests: Too many requests
POST /api/login
→ 429 Too Many Requests
Retry-After: 60
{"error": "Too many login attempts. Try again in 60 seconds"}
Rate limiting. Defends against DDoS and brute force attacks.
500 Internal Server Error: Server crashed
// Server dies from unhandled exception
app.get('/posts', (req, res) => {
const posts = null;
res.json(posts.filter(p => p.published)); // 💥 Cannot read property 'filter' of null
});
502 Bad Gateway: Gateway (proxy) problem
nginx (reverse proxy)
→ backend server (no response)
→ 502 Bad Gateway
Backend is dead or responding too slowly.
503 Service Unavailable: Server overload
→ 503 Service Unavailable
Retry-After: 120
Server under maintenance or too much traffic. Retry-After says "come back in 120 seconds."
This was the hardest concept to grasp. "Why intentionally make it inconvenient?"
1. Client: "Logging in. ID: admin, PW: 1234"
2. Server: "Confirmed. Login successful!"
(1 second later)
3. Client: "Show my profile"
4. Server: "Who are you? Please log in first"
Server forgets login from 1 second ago. This is Stateless.
If Stateful, this happens:
Server A: "John logged in. Must remember"
→ Stores in memory: { user: "John", logged_in: true }
(Next request goes to Server B)
Server B: "John? Who's that? Don't know"
When load balancer distributes requests across servers, Server B doesn't know what Server A remembered.
Solution 1: Sticky SessionLoad balancer: "Always send John to Server A"
Problem: If Server A dies? John gets logged out.
Solution 2: Stateless + External StoreBoth Server A and Server B check Redis
Redis: { session_id: "abc123" → { user: "John" } }
Any server can check Redis. This is modern web architecture.
1 server → 10 servers → 100 servers
All work identically
2. Server Restart OK
Server update → restart
Users don't get logged out (sessions in DB/Redis)
3. Simplicity
Each request is independent
Easy debugging
I finally understood. Stateless is a tradeoff for scalability.
If the server can't remember, the client must. That's cookies.
1. Login success
Server → Client: "Set-Cookie: session_id=abc123"
2. Browser saves cookie
3. Next request
Client → Server: "Cookie: session_id=abc123"
(Browser auto-attaches)
4. Server: "abc123? Oh, John!" (DB lookup)
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/; Domain=.example.com
Initially I thought "why so many options?" Each one matters.
HttpOnly: Block JavaScript access
// ❌ Doesn't work
document.cookie; // session_id not visible
Defends against XSS. Prevents hackers from stealing cookies with JavaScript.
Secure: HTTPS only
HTTP requests don't send cookie
HTTPS requests send cookie
Defends against WiFi hacking.
SameSite: CSRF defense
SameSite=Strict: Only send cookie for same-site requests
SameSite=Lax: Link clicks OK, POST not OK
SameSite=None: Send cookie for cross-site (Secure required)
Max-Age: Lifetime (seconds)
Max-Age=3600 → Delete after 1 hour
Max-Age=0 → Delete immediately (logout)
Expires: Expiration date
Expires=Wed, 21 Oct 2026 07:28:00 GMT
Similar to Max-Age but specifies date. Max-Age takes precedence.
Domain: Which domain can use
Domain=.example.com
→ OK for example.com, www.example.com, api.example.com
Path: Which path can use
Path=/api
→ OK for /api/users, not OK for /login
Storing everything in cookies poses security risks. So store only Session ID in cookie, actual data on server (DB/Redis).
// Login handling
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !user.checkPassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
const sessionId = crypto.randomUUID();
await redis.set(sessionId, JSON.stringify({ userId: user.id }), 'EX', 3600); // 1 hour
// Set cookie
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000 // 1 hour (milliseconds)
});
res.json({ message: 'Login successful' });
});
// Auth middleware
async function authenticate(req, res, next) {
const sessionId = req.cookies.session_id;
if (!sessionId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const session = await redis.get(sessionId);
if (!session) {
return res.status(401).json({ error: 'Session expired' });
}
req.user = JSON.parse(session);
next();
}
// Protected route
app.get('/api/profile', authenticate, async (req, res) => {
const user = await User.findById(req.user.userId);
res.json(user);
});
When I first built a website, I wondered "why does the image download every time?" I didn't know about caching.
Cache-Control: max-age=3600, public
// Static files: 1 year cache
app.use('/static', express.static('public', {
maxAge: '1y'
}));
// API responses: no cache
app.get('/api/posts', (req, res) => {
res.set('Cache-Control', 'no-store');
res.json(posts);
});
1. First request
GET /api/posts
→ 200 OK
ETag: "abc123"
[post data]
2. Next request (with ETag)
GET /api/posts
If-None-Match: "abc123"
3a. If unchanged
→ 304 Not Modified
(no body, browser uses cache)
3b. If changed
→ 200 OK
ETag: "def456"
[new data]
304 Not Modified is true innovation. No body sent, saving massive bandwidth.
app.get('/api/posts', async (req, res) => {
const posts = await Post.findAll();
const etag = crypto.createHash('md5').update(JSON.stringify(posts)).digest('hex');
// Compare with client's ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Just 304, no body
}
res.set('ETag', etag);
res.set('Cache-Control', 'max-age=60');
res.json(posts);
});
1. First request
GET /api/posts
→ 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
2. Next request
GET /api/posts
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
3. If unchanged
→ 304 Not Modified
Less precise than ETag but simpler.
Same URL can return JSON or HTML. Depends on what client wants.
GET /api/posts
Accept: application/json
→ JSON response
GET /api/posts
Accept: text/html
→ HTML response
app.get('/api/posts', async (req, res) => {
const posts = await Post.findAll();
if (req.accepts('json')) {
res.json(posts);
} else if (req.accepts('html')) {
res.render('posts', { posts });
} else {
res.status(406).send('Not Acceptable');
}
});
GET /api/posts
Accept-Encoding: gzip, br
→ Compress response with gzip or Brotli
const compression = require('compression');
app.use(compression()); // Automatic gzip compression
Text compresses 70-90%. JSON 10KB → gzip 1KB. Huge savings.
GET /api/posts
Accept-Language: ko-KR, en-US
→ Korean if available, otherwise English
Login at cafe WiFi
Client → Router: "GET /login?password=1234"
↓
Hacker captures WiFi packets
↓
Hacker: "Password is 1234? lol"
Plaintext transmission risks:
Client → Server: (encrypted data)
↓
Hacker captures packets
↓
Hacker: "aX93jK2... what is this? Can't read"
What HTTPS solves:
1. Client: "Starting HTTPS connection"
2. Server: "Here's my certificate" (includes public key)
3. Client: Verifies certificate (signed by CA?)
4. Client: Generates symmetric key → encrypts with server's public key → sends
5. Server: Decrypts with private key → obtains symmetric key
6. Now encrypted communication with symmetric key
Actual API server code I built:
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const compression = require('compression');
const redis = require('redis');
const crypto = require('crypto');
const app = express();
const redisClient = redis.createClient();
// Middleware
app.use(express.json());
app.use(cookieParser());
app.use(compression()); // gzip compression
app.use(cors({
origin: 'https://example.com',
credentials: true // Allow cookies
}));
// Login
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Validation
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
// User verification (actual: DB lookup)
const user = { id: 1, email: 'user@example.com', name: 'John' };
// Create session
const sessionId = crypto.randomUUID();
await redisClient.setEx(
`session:${sessionId}`,
3600, // 1 hour
JSON.stringify(user)
);
// Set cookie
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000
});
res.status(200).json({ message: 'Login successful', user });
});
// Auth middleware
async function authenticate(req, res, next) {
const sessionId = req.cookies.session_id;
if (!sessionId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const session = await redisClient.get(`session:${sessionId}`);
if (!session) {
return res.status(401).json({ error: 'Session expired' });
}
req.user = JSON.parse(session);
next();
}
// Post list (caching + ETag)
app.get('/api/posts', async (req, res) => {
const posts = [
{ id: 1, title: 'First post', author: 'John' },
{ id: 2, title: 'Second post', author: 'Jane' }
];
// Generate ETag
const etag = crypto.createHash('md5')
.update(JSON.stringify(posts))
.digest('hex');
// 304 Not Modified handling
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.set({
'ETag': etag,
'Cache-Control': 'max-age=300, public' // 5 min cache
});
res.json(posts);
});
// Create post (auth required)
app.post('/api/posts', authenticate, async (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: 'Title and content required' });
}
const post = {
id: Date.now(),
title,
content,
author: req.user.name,
createdAt: new Date()
};
// Save to DB (omitted)
res.status(201)
.set('Location', `/api/posts/${post.id}`)
.json(post);
});
// Fetch post (caching)
app.get('/api/posts/:id', async (req, res) => {
const post = { id: req.params.id, title: 'Post title' };
res.set({
'Cache-Control': 'max-age=3600, public',
'Last-Modified': new Date('2025-01-01').toUTCString()
});
res.json(post);
});
// Update post (auth required)
app.patch('/api/posts/:id', authenticate, async (req, res) => {
const { title } = req.body;
// Permission check (omitted)
const updatedPost = { id: req.params.id, title };
res.json(updatedPost);
});
// Delete post (auth required)
app.delete('/api/posts/:id', authenticate, async (req, res) => {
// Delete handling (omitted)
res.status(204).end(); // No Content
});
// Rate limiting
const rateLimits = new Map();
app.post('/api/login-limited', async (req, res) => {
const ip = req.ip;
const now = Date.now();
const windowMs = 60000; // 1 min
const maxRequests = 5;
if (!rateLimits.has(ip)) {
rateLimits.set(ip, []);
}
const requests = rateLimits.get(ip).filter(time => now - time < windowMs);
if (requests.length >= maxRequests) {
return res.status(429)
.set('Retry-After', '60')
.json({ error: 'Too many requests' });
}
requests.push(now);
rateLimits.set(ip, requests);
// Login handling...
res.json({ message: 'OK' });
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Frontend HTTP request code:
// GET request
async function getPosts() {
const response = await fetch('https://api.example.com/posts', {
method: 'GET',
headers: {
'Accept': 'application/json'
},
credentials: 'include' // Include cookies
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check caching headers
const cacheControl = response.headers.get('Cache-Control');
const etag = response.headers.get('ETag');
console.log('Cache:', cacheControl, 'ETag:', etag);
return response.json();
}
// POST request (login)
async function login(email, password) {
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include', // Receive cookies
body: JSON.stringify({ email, password })
});
if (response.status === 401) {
throw new Error('Invalid credentials');
}
if (!response.ok) {
throw new Error('Login failed');
}
return response.json();
}
// Authenticated request
async function getProfile() {
const response = await fetch('https://api.example.com/profile', {
credentials: 'include' // Auto-send cookies
});
if (response.status === 401) {
// Redirect to login
window.location.href = '/login';
return;
}
return response.json();
}
// Conditional request with ETag
let cachedPosts = null;
let cachedETag = null;
async function getPostsWithCache() {
const headers = {
'Accept': 'application/json'
};
if (cachedETag) {
headers['If-None-Match'] = cachedETag;
}
const response = await fetch('https://api.example.com/posts', {
headers,
credentials: 'include'
});
if (response.status === 304) {
console.log('Using cached data');
return cachedPosts; // Use cached data
}
cachedETag = response.headers.get('ETag');
cachedPosts = await response.json();
return cachedPosts;
}
// Error handling
async function robustFetch(url, options) {
try {
const response = await fetch(url, options);
// Handle by status code
switch (response.status) {
case 200:
case 201:
return response.json();
case 204:
return null; // No Content
case 304:
return { cached: true };
case 400:
const error = await response.json();
throw new Error(`Validation error: ${error.message}`);
case 401:
window.location.href = '/login';
break;
case 403:
throw new Error('Permission denied');
case 404:
throw new Error('Resource not found');
case 429:
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limited. Retry after ${retryAfter}s`);
case 500:
case 502:
case 503:
throw new Error('Server error. Please try again later.');
default:
throw new Error(`HTTP ${response.status}`);
}
} catch (err) {
if (err.name === 'TypeError') {
// Network error
throw new Error('Network error. Check your connection.');
}
throw err;
}
}
When first building login functionality, I did this:
// ❌ Never do this
async function login(email, password) {
const response = await fetch(`/login?email=${email}&password=${password}`);
return response.json();
}
Problems:
[GET] /login?password=1234)// ✅ Correct way
async function login(email, password) {
const response = await fetch('https://example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return response.json();
}
Frontend (localhost:3000) → Backend (localhost:4000):
Access to fetch at 'http://localhost:4000/api' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Initially thought "why doesn't same computer work?" Different ports = different origins.
Solution:
// Backend
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
credentials: true // Allow cookies
}));
// Frontend
fetch('http://localhost:4000/api', {
credentials: 'include' // Send cookies
});
// ❌ Strangely mixed
fetch('/api/posts?category=tech', {
method: 'POST',
body: JSON.stringify({ title: 'Title' })
});
"Why is category in query string?" Confusing later.
Guidelines:
GET /api/posts?category=tech&page=1
GET /api/posts/123
DELETE /api/users/456
POST /api/posts
{ "title": "Title", "content": "Content" }
Updated a post but frontend didn't show changes. Browser was using cache.
Solution:
// Invalidate cache after update/delete
app.patch('/api/posts/:id', async (req, res) => {
// Update...
res.set('Cache-Control', 'no-cache'); // Don't use cache
res.json(updatedPost);
});
Or change ETag so browser knows "oh, it changed."
// ❌ Doesn't work
const sessionId = document.cookie; // session_id not visible
HttpOnly cookies block JavaScript access. Browser auto-attaches them, no need to worry.
// ❌ No exception handling
app.get('/api/posts', (req, res) => {
const posts = null;
res.json(posts.filter(p => p.published)); // 💥 Server dies
});
// ✅ try-catch
app.get('/api/posts', async (req, res) => {
try {
const posts = await Post.findAll();
res.json(posts);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
What I learned from studying HTTP:
Initially thought "just data exchange." Now I understand HTTP is the language of the web. Every request header, every cookie attribute has a reason. Understanding them enables building safer, faster web applications.
The "amnesiac mailman" analogy made everything click. Understanding HTTP meant understanding how the web works.