Server Basics — HTTP Servers and Requests

Status: Active | Last Updated: 2026-08-29 Category: Backend — HTTP Fundamentals Prerequisites: TypeScript basics; Bun runtime basics Tags: server, http, backend, response, request, status-codes Estimated Time: 3-4 hours (Self-paced, includes lab time)

Summary

Before you build APIs, you must understand what an HTTP server does: receive a request, parse it, decide what response to send, and return it. This article explains the request/response lifecycle, status codes, headers, and how Bun's native Bun.serve() fits into that model.

What You'll Learn (Core Competencies)

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

HTTP (Hypertext Transfer Protocol) is stateless — each request is independent. A client (browser, another server, a script) sends a request to a server; the server responds. That's the entire cycle. No memory of previous requests unless the server or client explicitly stores it (cookies, sessions, database records).

The protocol operates over TCP, usually on port 80 for HTTP and 443 for HTTPS. A single TCP connection can handle multiple sequential requests (persistent connections in HTTP/1.1), but the server treats each as independent. That simplicity is why HTTP scales so well: every server can handle thousands of disconnected clients without tracking conversation history.

In practice, statelessness means authentication must be repeated. A cookie or JWT token accompanies every request; the server validates it fresh each time rather than remembering that the user logged in earlier. This design makes load balancing trivial — any server can answer any request — but forces applications to manage state explicitly when needed.

Every HTTP request contains:

The lifecycle breaks down into discrete steps: DNS resolution turns a hostname into an IP; a TCP handshake establishes a connection; the client sends the HTTP text over the wire; the server parses the request line (method, path, protocol version), then the headers, then reads the body if present. Only after full parsing does the server invoke application code. That separation between transport and logic is what lets frameworks handle headers, compression, and TLS without touching business rules.

2. Deep Dive & Implementation

Inside Bun, the fetch handler receives a standard Web Request object. That object exposes request.method, request.url, request.headers, and request.body. Because Bun uses the Web API, skills learned here transfer directly to browsers, workers, and edge functions.

Status Codes

Status codes tell the client what happened. Grouped by the first digit:

Using the right code makes APIs predictable. Return 201 when a POST creates something new, not 200. Return 404 when a resource is genuinely missing, not 500. A well-chosen status code lets clients decide whether to retry (500), adjust input (400), or stop (404).

In Bun, set the code explicitly:

Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/users/42" && request.method === "GET") {
      return new Response("Found user 42", { status: 200 });
    }
    if (request.method === "POST" && url.pathname === "/users") {
      return Response.json({ id: 42 }, { status: 201 });
    }
    return new Response("Not Found", { status: 404 });
  }
});

Headers

Headers provide metadata about the request or response. Common request headers: Content-Type (format of body), Authorization (auth token), Accept (preferred response format), User-Agent. Common response headers: Content-Type, Cache-Control, Access-Control-Allow-Origin (CORS), Set-Cookie.

CORS headers are essential when a browser loads a page from one origin and calls an API on another. Without Access-Control-Allow-Origin, browsers block the response. Always include this header in development APIs that serve frontend clients from a different port or domain.

Reading headers in Bun is straightforward:

Bun.serve({
  port: 3000,
  fetch(request) {
    const auth = request.headers.get("Authorization");
    const contentType = request.headers.get("Content-Type");
    console.log("Auth header:", auth, "Content-Type:", contentType);

    return new Response("Headers logged", {
      headers: {
        "Content-Type": "text/plain",
        "Access-Control-Allow-Origin": "*",
      }
    });
  }
});

Headers are case-insensitive string pairs. The Headers API treats names case-insensitively, so get("authorization") and get("Authorization") match the same value.

Complete Production-Style Server

Putting it together, a minimal server that logs every request, handles a health endpoint, parses JSON safely, and returns structured error responses:

// server.ts
Bun.serve({
  port: 3000,
  hostname: "0.0.0.0",
  async fetch(request) {
    const url = new URL(request.url);
    const method = request.method;

    console.log(`[${method}] ${url.pathname}`);

    // Health check
    if (url.pathname === "/health" && method === "GET") {
      return Response.json({ status: "ok", time: new Date().toISOString() });
    }

    // Read JSON body for POST
    if (url.pathname === "/posts" && method === "POST") {
      try {
        const body = await request.json();
        return Response.json({ created: true, post: body }, { status: 201 });
      } catch (e) {
        return Response.json({ error: "Invalid JSON" }, { status: 400 });
      }
    }

    // Generic 404
    return new Response("Not Found", { status: 404, headers: { "Content-Type": "text/plain" } });
  }
});

console.log("Server running at http://localhost:3000");

The hostname: "0.0.0.0" allows external connections, useful inside containers or cloud environments.

Guided Checkpoint

Run the server and verify the response shapes with curl:

# Start the server: bun run server.ts
# In another terminal:

# Health check returns 200 with JSON body
curl -s http://localhost:3000/health | jq .

# POST with JSON body returns 201 and the parsed payload
curl -s -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello"}' | jq .

# GET on /posts returns 404 (not yet implemented)
curl -s -w "\n%{http_code}" http://localhost:3000/posts

# POST with malformed JSON returns 400
curl -s -w "\n%{http_code}" -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d 'not-valid-json'

All responses should have Content-Type: application/json or text/plain as appropriate, and status codes should match the method/path semantics described above.

3. Anti-Patterns & Common Pitfalls

Returning 200 OK for errors. A request that fails validation should return 400, not 200 with { error: "..." } in the body. Clients using the status code to decide how to handle responses will silently ignore the error.

Returning 500 for missing resources. A 404 means the resource does not exist; a 500 means the server encountered an unexpected internal error. Mixing these up makes debugging harder and can mask missing routes.

Not validating Content-Type before parsing. If a client sends Content-Type: application/json but the body is plain text, request.json() will throw. Always wrap in try/catch and return 400 on failure. Conversely, if a client sends raw text but the header says application/json, Bun will still try to parse it — check the header before calling request.json().

Logging sensitive data in headers. Authorization headers, cookies, and custom auth headers often contain secrets. Logging request.headers directly can leak credentials to log files. Always log header names (keys) but redact values for sensitive headers.

Ignoring the request method. A GET /users and a POST /users should do different things. If you only check the path and ignore request.method, you will mishandle requests and violate HTTP semantics.

Missing the OPTIONS handler for CORS preflight. Browsers send an OPTIONS request before making cross-origin POST/PUT/DELETE requests. If your server doesn't return CORS headers on OPTIONS, the actual request will be blocked.

4. Independent Challenge

Design a request router that handles at least four routes with different methods and body types. The router must:

Do not use a router library. Build it with if/else chains on request.method and url.pathname as shown in the examples above.

5. Consolidation & Key Invariants

6. Next Steps


Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse