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
PartWhat it tells the server
MethodWhat action the client wants (GET, POST, PUT, DELETE)
PathWhich resource the client wants (includes query string)
HTTP VersionWhich version of the protocol is being used

HTTP methods in detail

MethodPurposeHas BodyIdempotentSafe
GETRetrieve a resourceNoYesYes
POSTCreate a new resourceYesNoNo
PUTReplace an entire resourceYesYesNo
PATCHPartially update a resourceYesNoNo
DELETERemove a resourceOptionalYesNo
HEADSame as GET but no body in responseNoYesYes
OPTIONSAsk what methods are allowedNoYesYes

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
PartMeaning
HTTP VersionProtocol version
Status CodeNumeric code indicating the result (200, 404, 500, etc.)
Reason PhraseHuman-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 TypeDescriptionTypical Use
text/plainPlain textSimple text responses, logs
text/htmlHTML documentWeb pages
text/cssCSS stylesheetStyles
text/csvComma-separated valuesData exports
application/jsonJSON dataAPI responses
application/javascriptJavaScript codeScript files
application/xmlXML dataLegacy APIs, feeds
application/pdfPDF documentDocuments
application/octet-streamBinary data (generic)File downloads
image/pngPNG imageGraphics with transparency
image/jpegJPEG imagePhotos
image/svg+xmlSVG imageScalable vector graphics
image/gifGIF imageAnimated images
audio/mpegMP3 audioMusic, podcasts
video/mp4MP4 videoVideo content
multipart/form-dataForm with file uploadsFile uploads from HTML forms
application/x-www-form-urlencodedURL-encoded form dataSimple 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');
DirectiveMeaning
publicAny cache can store this
privateOnly the user's browser can cache this
max-age=NCache for N seconds
no-storeDo not store the response anywhere
no-cacheStore it, but revalidate before using
must-revalidateOnce 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 AttributePurpose
HttpOnlyCookie cannot be accessed by JavaScript (XSS protection)
SecureCookie only sent over HTTPS
Path=/Cookie applies to all paths
Max-Age=86400Cookie expires in 86400 seconds (24 hours)
SameSite=StrictCookie 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

HeaderPurposeExample Value
HostTarget server hostnameexample.com
AcceptAcceptable response formatstext/html, application/json
Content-TypeFormat of request bodyapplication/json
AuthorizationAuthentication credentialsBearer <token>
User-AgentClient software identifierMozilla/5.0...
CookieCookies from previous responsessessionId=abc123
Accept-LanguagePreferred languagesen-US,en;q=0.9
Accept-EncodingSupported compressiongzip, deflate, br
RefererURL of the previous pagehttps://google.com
OriginOrigin 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

  1. Always set the correct status code — do not send 200 for errors.
  2. Always include a message — the client needs to know what went wrong.
  3. Never expose internal details — do not send stack traces or database errors to the client.
  4. 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

  1. An HTTP request has four parts: request line, headers, empty line, and body.
  2. An HTTP response has four parts: status line, headers, empty line, and body.
  3. Content-Type (MIME type) tells the receiver how to interpret the data — always set it.
  4. Response headers like Cache-Control, Set-Cookie, and Content-Length control caching, sessions, and transfer behavior.
  5. Request headers like Accept, Authorization, and User-Agent tell the server about the client's needs and identity.
  6. Use fs.readFile with http to serve static HTML files — always use path.join and guard against directory traversal.
  7. Consistent JSON response formats ({ success, data } or { success, error }) make APIs predictable and easy to consume.
  8. 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:

  1. Serves the correct file for each URL (/ serves index.html).
  2. Sets the correct Content-Type for each file extension.
  3. Returns a styled 404 page when a file does not exist.
  4. Protects against directory traversal attacks.
  5. 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