Episode 3 — NodeJS MongoDB Backend Architecture / 3.2 — Creating Server
3.2.c — Serving Responses and Understanding HTTP
HTTP is the language that clients and servers speak — understanding its anatomy is essential to building reliable web applications.
Navigation: ← 3.2 Overview | Next → 3.2.d Routing in HTTP Servers
1. HTTP Request Anatomy
Every HTTP request consists of four parts: the request line, headers, an empty line, and an optional body.
┌─────────────────────────────────────────────────────┐
│ REQUEST LINE │
│ GET /api/users?page=2 HTTP/1.1 │
├─────────────────────────────────────────────────────┤
│ HEADERS │
│ Host: example.com │
│ Accept: application/json │
│ Authorization: Bearer eyJhbGciOi... │
│ User-Agent: Mozilla/5.0 │
│ Content-Type: application/json │
├─────────────────────────────────────────────────────┤
│ EMPTY LINE (separates headers from body) │
│ │
├─────────────────────────────────────────────────────┤
│ BODY (optional — usually for POST/PUT) │
│ {"name": "Alice", "email": "alice@example.com"} │
└─────────────────────────────────────────────────────┘
The request line
The first line of every request has three parts:
METHOD PATH HTTP_VERSION
GET /api/users?page=2 HTTP/1.1
| Part | What it tells the server |
|---|---|
| Method | What action the client wants (GET, POST, PUT, DELETE) |
| Path | Which resource the client wants (includes query string) |
| HTTP Version | Which version of the protocol is being used |
HTTP methods in detail
| Method | Purpose | Has Body | Idempotent | Safe |
|---|---|---|---|---|
GET | Retrieve a resource | No | Yes | Yes |
POST | Create a new resource | Yes | No | No |
PUT | Replace an entire resource | Yes | Yes | No |
PATCH | Partially update a resource | Yes | No | No |
DELETE | Remove a resource | Optional | Yes | No |
HEAD | Same as GET but no body in response | No | Yes | Yes |
OPTIONS | Ask what methods are allowed | No | Yes | Yes |
Idempotent means calling it multiple times produces the same result. Sending the same PUT request 10 times should yield the same state as sending it once.
Safe means the request does not modify anything on the server.
2. HTTP Response Anatomy
The server's response also has a defined structure.
┌─────────────────────────────────────────────────────┐
│ STATUS LINE │
│ HTTP/1.1 200 OK │
├─────────────────────────────────────────────────────┤
│ RESPONSE HEADERS │
│ Content-Type: application/json │
│ Content-Length: 256 │
│ Cache-Control: max-age=3600 │
│ Set-Cookie: session=abc123; HttpOnly │
│ Date: Sat, 15 Mar 2025 10:30:00 GMT │
├─────────────────────────────────────────────────────┤
│ EMPTY LINE │
│ │
├─────────────────────────────────────────────────────┤
│ RESPONSE BODY │
│ {"users": [{"id": 1, "name": "Alice"}]} │
└─────────────────────────────────────────────────────┘
The status line
HTTP_VERSION STATUS_CODE REASON_PHRASE
HTTP/1.1 200 OK
| Part | Meaning |
|---|---|
| HTTP Version | Protocol version |
| Status Code | Numeric code indicating the result (200, 404, 500, etc.) |
| Reason Phrase | Human-readable description of the status code |
3. Content-Type and MIME Types Explained
MIME (Multipurpose Internet Mail Extensions) types tell the client what kind of data is in the response body.
Format
type/subtype
Common MIME types
| MIME Type | Description | Typical Use |
|---|---|---|
text/plain | Plain text | Simple text responses, logs |
text/html | HTML document | Web pages |
text/css | CSS stylesheet | Styles |
text/csv | Comma-separated values | Data exports |
application/json | JSON data | API responses |
application/javascript | JavaScript code | Script files |
application/xml | XML data | Legacy APIs, feeds |
application/pdf | PDF document | Documents |
application/octet-stream | Binary data (generic) | File downloads |
image/png | PNG image | Graphics with transparency |
image/jpeg | JPEG image | Photos |
image/svg+xml | SVG image | Scalable vector graphics |
image/gif | GIF image | Animated images |
audio/mpeg | MP3 audio | Music, podcasts |
video/mp4 | MP4 video | Video content |
multipart/form-data | Form with file uploads | File uploads from HTML forms |
application/x-www-form-urlencoded | URL-encoded form data | Simple HTML form submissions |
Character encoding
For text types, you should specify the character encoding:
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
Without charset=utf-8, special characters (accented letters, emojis, non-Latin scripts) may not display correctly.
4. Response Headers
Response headers provide metadata about the response.
Content-Length
Tells the client the size of the response body in bytes.
const body = JSON.stringify({ message: 'hello' });
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body) // Use Buffer for accurate byte count
});
res.end(body);
Why Buffer.byteLength instead of body.length? Because string.length counts characters, but multi-byte characters (like emojis) use more than one byte.
Cache-Control
Tells the browser whether and how long to cache the response.
// Cache for 1 hour
res.setHeader('Cache-Control', 'public, max-age=3600');
// Do not cache at all
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
// Cache but revalidate with server each time
res.setHeader('Cache-Control', 'no-cache');
| Directive | Meaning |
|---|---|
public | Any cache can store this |
private | Only the user's browser can cache this |
max-age=N | Cache for N seconds |
no-store | Do not store the response anywhere |
no-cache | Store it, but revalidate before using |
must-revalidate | Once expired, must check with server |
Set-Cookie
Sends a cookie to the client's browser.
res.writeHead(200, {
'Content-Type': 'text/html',
'Set-Cookie': 'sessionId=abc123; HttpOnly; Secure; Path=/; Max-Age=86400'
});
| Cookie Attribute | Purpose |
|---|---|
HttpOnly | Cookie cannot be accessed by JavaScript (XSS protection) |
Secure | Cookie only sent over HTTPS |
Path=/ | Cookie applies to all paths |
Max-Age=86400 | Cookie expires in 86400 seconds (24 hours) |
SameSite=Strict | Cookie not sent with cross-site requests (CSRF protection) |
Other important response headers
// Tell the client the server type
res.setHeader('X-Powered-By', 'Node.js');
// Allow cross-origin requests (CORS)
res.setHeader('Access-Control-Allow-Origin', '*');
// Redirect the client
res.writeHead(301, { 'Location': 'https://new-url.com/page' });
res.end();
5. Request Headers
Headers the client sends to the server.
Accept
Tells the server what content types the client can handle.
// In your server, check what the client wants:
const server = http.createServer((req, res) => {
const acceptHeader = req.headers['accept'];
if (acceptHeader && acceptHeader.includes('application/json')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello' }));
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello</h1>');
}
});
Content-Type (on requests)
When the client sends data (POST/PUT), this header tells the server the format of the body.
Content-Type: application/json → Body is JSON
Content-Type: application/x-www-form-urlencoded → Body is form data
Content-Type: multipart/form-data → Body contains files
Authorization
Carries credentials for authentication.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
User-Agent
Identifies the client software making the request.
const server = http.createServer((req, res) => {
const userAgent = req.headers['user-agent'];
console.log('Request from:', userAgent);
// "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
// "curl/7.79.1"
// "PostmanRuntime/7.29.2"
res.end('OK');
});
Summary table of request headers
| Header | Purpose | Example Value |
|---|---|---|
Host | Target server hostname | example.com |
Accept | Acceptable response formats | text/html, application/json |
Content-Type | Format of request body | application/json |
Authorization | Authentication credentials | Bearer <token> |
User-Agent | Client software identifier | Mozilla/5.0... |
Cookie | Cookies from previous responses | sessionId=abc123 |
Accept-Language | Preferred languages | en-US,en;q=0.9 |
Accept-Encoding | Supported compression | gzip, deflate, br |
Referer | URL of the previous page | https://google.com |
Origin | Origin of the request (CORS) | https://myapp.com |
6. Serving Different Content Types from the Same Server
A single server can handle different types of content based on the URL or the Accept header.
const http = require('http');
const server = http.createServer((req, res) => {
const url = req.url;
// Serve HTML page
if (url === '/' || url === '/home') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<body>
<h1>Welcome to My Server</h1>
<p>Visit <a href="/api/data">/api/data</a> for JSON.</p>
<p>Visit <a href="/style.css">/style.css</a> for CSS.</p>
</body>
</html>
`);
}
// Serve JSON API data
else if (url === '/api/data') {
const data = {
users: ['Alice', 'Bob', 'Charlie'],
count: 3,
timestamp: Date.now()
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data, null, 2));
}
// Serve CSS
else if (url === '/style.css') {
res.writeHead(200, { 'Content-Type': 'text/css' });
res.end(`
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
`);
}
// Serve plain text
else if (url === '/robots.txt') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('User-agent: *\nDisallow: /api/');
}
// 404 for everything else
else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
}
});
server.listen(3000, () => {
console.log('Multi-content server running on http://localhost:3000');
});
7. Serving Static HTML Files Using fs.readFile + http
Instead of embedding HTML in your JavaScript, read it from actual .html files.
Project structure
project/
├── server.js
└── public/
├── index.html
├── about.html
└── 404.html
server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
// Map URLs to file paths
let filePath;
switch (req.url) {
case '/':
filePath = path.join(__dirname, 'public', 'index.html');
break;
case '/about':
filePath = path.join(__dirname, 'public', 'about.html');
break;
default:
filePath = path.join(__dirname, 'public', '404.html');
}
// Read the file from disk
fs.readFile(filePath, 'utf8', (err, content) => {
if (err) {
// File read error — send 500
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
return;
}
// Determine status code
const statusCode = req.url === '/' || req.url === '/about' ? 200 : 404;
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(content);
});
});
server.listen(3000, () => {
console.log('Static file server running on http://localhost:3000');
});
A more dynamic approach — serve any file from public/
const http = require('http');
const fs = require('fs');
const path = require('path');
// Map file extensions to MIME types
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain'
};
const server = http.createServer((req, res) => {
// Build the file path
let urlPath = req.url === '/' ? '/index.html' : req.url;
let filePath = path.join(__dirname, 'public', urlPath);
// Security: prevent directory traversal attacks
if (!filePath.startsWith(path.join(__dirname, 'public'))) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Forbidden');
return;
}
// Get the file extension and MIME type
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// Read and serve the file
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
// File not found
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404 — File Not Found</h1>');
} else {
// Server error
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
});
});
server.listen(3000, () => {
console.log('Static file server running on http://localhost:3000');
});
Why path.join instead of string concatenation?
// BAD: breaks on different operating systems
const filePath = __dirname + '/public/' + req.url;
// GOOD: works on Windows, Mac, and Linux
const filePath = path.join(__dirname, 'public', req.url);
path.join handles path separators (/ vs \) correctly across operating systems.
Security: directory traversal attack
Without the security check above, an attacker could request:
GET /../../../etc/passwd
This would read files outside your public/ directory. Always validate that the resolved path stays within the intended directory.
8. JSON API Responses — Formatting and Headers
When building APIs, consistent JSON responses make your frontend developer's life easier.
Standard success response
function sendSuccess(res, data, statusCode = 200) {
const response = {
success: true,
data: data,
timestamp: new Date().toISOString()
};
const body = JSON.stringify(response, null, 2);
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body)
});
res.end(body);
}
// Usage:
sendSuccess(res, { users: [{ id: 1, name: 'Alice' }] });
Standard error response
function sendError(res, message, statusCode = 500) {
const response = {
success: false,
error: {
message: message,
code: statusCode
},
timestamp: new Date().toISOString()
};
const body = JSON.stringify(response, null, 2);
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body)
});
res.end(body);
}
// Usage:
sendError(res, 'User not found', 404);
sendError(res, 'Invalid email format', 400);
sendError(res, 'Internal server error', 500);
Complete API server example
const http = require('http');
// In-memory data store
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'user' }
];
function sendJSON(res, data, statusCode = 200) {
const body = JSON.stringify(data, null, 2);
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
'Access-Control-Allow-Origin': '*'
});
res.end(body);
}
const server = http.createServer((req, res) => {
if (req.url === '/api/users' && req.method === 'GET') {
sendJSON(res, {
success: true,
count: users.length,
data: users
});
}
else if (req.url.startsWith('/api/users/') && req.method === 'GET') {
const id = parseInt(req.url.split('/')[3]);
const user = users.find(u => u.id === id);
if (user) {
sendJSON(res, { success: true, data: user });
} else {
sendJSON(res, { success: false, error: 'User not found' }, 404);
}
}
else {
sendJSON(res, { success: false, error: 'Route not found' }, 404);
}
});
server.listen(3000, () => {
console.log('API server on http://localhost:3000');
});
9. Error Responses — Sending Proper Error Messages
Proper error handling separates amateur servers from professional ones.
The golden rules of error responses
- Always set the correct status code — do not send 200 for errors.
- Always include a message — the client needs to know what went wrong.
- Never expose internal details — do not send stack traces or database errors to the client.
- Be consistent — use the same error format everywhere.
Example: handling different error types
const http = require('http');
const server = http.createServer((req, res) => {
try {
if (req.method !== 'GET') {
// 405 Method Not Allowed
res.writeHead(405, {
'Content-Type': 'application/json',
'Allow': 'GET' // Tell the client which methods ARE allowed
});
res.end(JSON.stringify({
error: 'Method Not Allowed',
message: `${req.method} is not supported. Use GET.`
}));
return;
}
if (req.url === '/api/secret' && !req.headers['authorization']) {
// 401 Unauthorized
res.writeHead(401, {
'Content-Type': 'application/json',
'WWW-Authenticate': 'Bearer'
});
res.end(JSON.stringify({
error: 'Unauthorized',
message: 'Authorization header is required.'
}));
return;
}
if (req.url === '/api/admin' && req.headers['authorization'] !== 'Bearer admin-token') {
// 403 Forbidden
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Forbidden',
message: 'You do not have permission to access this resource.'
}));
return;
}
if (req.url === '/api/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ users: ['Alice', 'Bob'] }));
return;
}
// 404 Not Found — catch-all for unknown routes
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Not Found',
message: `The route ${req.url} does not exist.`
}));
} catch (error) {
// 500 Internal Server Error — something unexpected happened
console.error('Server error:', error); // Log the real error
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal Server Error',
message: 'Something went wrong. Please try again later.'
// DO NOT include: error.stack, error.message, database details
}));
}
});
server.listen(3000, () => {
console.log('Server with error handling on http://localhost:3000');
});
Testing error responses with curl
# 200 OK
curl http://localhost:3000/api/users
# 404 Not Found
curl http://localhost:3000/api/nonexistent
# 405 Method Not Allowed
curl -X POST http://localhost:3000/api/users
# 401 Unauthorized
curl http://localhost:3000/api/secret
# 403 Forbidden (wrong token)
curl -H "Authorization: Bearer wrong" http://localhost:3000/api/admin
Key Takeaways
- An HTTP request has four parts: request line, headers, empty line, and body.
- An HTTP response has four parts: status line, headers, empty line, and body.
Content-Type(MIME type) tells the receiver how to interpret the data — always set it.- Response headers like
Cache-Control,Set-Cookie, andContent-Lengthcontrol caching, sessions, and transfer behavior. - Request headers like
Accept,Authorization, andUser-Agenttell the server about the client's needs and identity. - Use
fs.readFilewithhttpto serve static HTML files — always usepath.joinand guard against directory traversal. - Consistent JSON response formats (
{ success, data }or{ success, error }) make APIs predictable and easy to consume. - Never expose internal error details to clients — log them server-side, send generic messages to users.
Explain-It Challenge
Build a mini file server:
Create a project with a public/ folder containing index.html, style.css, and script.js. Write a server that:
- Serves the correct file for each URL (
/servesindex.html). - Sets the correct
Content-Typefor each file extension. - Returns a styled 404 page when a file does not exist.
- Protects against directory traversal attacks.
- Logs each request with the method, URL, status code, and response time.
Then explain to a study partner: why does Content-Type matter? What happens if you send JSON with Content-Type: text/html?
Navigation: ← 3.2 Overview | Next → 3.2.d Routing in HTTP Servers