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)
- Explain the stateless HTTP request/response cycle and why it matters for scaling
- Parse HTTP requests: method, URL, headers, and optional body using Bun's Web API
- Return correctly typed responses with appropriate status codes and headers
- Implement a minimal production-style server with health endpoint and error handling
Table of Contents
- Architectural Overview & Core Schema
- Deep Dive & Implementation
- Anti-Patterns & Common Pitfalls
- Independent Challenge
- Consolidation & Key Invariants
- 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:
- A method: GET (read), POST (create), PUT (update), DELETE (remove)
- A URL path:
/posts/42 - Headers: metadata like
Content-Type: application/json - A body (optional): the payload, usually JSON or form data
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:
- 2xx Success:
200 OK(found),201 Created(new resource made),204 No Content(deleted or empty response) - 3xx Redirection:
301 Moved Permanently,302 Found,304 Not Modified - 4xx Client Error:
400 Bad Request(bad syntax),401 Unauthorized(needs auth),403 Forbidden(no permission),404 Not Found,409 Conflict(duplicate) - 5xx Server Error:
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable
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:
- Return
200with{ method, path }forGET /echo - Return
201with a parsed JSON body forPOST /echo - Return
400whenPOST /echoreceives non-JSON - Return
404for any other path - Log every incoming request's method and pathname
- Include a
/healthendpoint that returns server uptime
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
- HTTP is stateless: every request must carry everything the server needs to handle it, including auth credentials
- Return the correct status code for each outcome:
200/201for success,400for client error,404for missing resources,500for unexpected server errors - Always wrap
request.json()in try/catch and return400on parse failure - Check
request.methodexplicitly for every route; never ignore the HTTP verb - Log request method and pathname, but never log raw Authorization header values
- Handle
OPTIONSrequests for CORS preflight by returning appropriateAccess-Control-*headers - Return
404for every unmatched route; do not return200with an empty body
6. Next Steps
- Read
kb/backend/bun-http.mdto learnBun.serve()patterns, WebSockets, and streaming. - Read
kb/backend/routing-patterns.mdfor structured route design and URL parameter extraction. - Read
kb/backend/middleware-patterns.mdfor auth, logging, and error-handling middleware chains. - Build a small REST API using the patterns above and deploy it with Docker (
kb/containers/docker-concepts.md).
Change Log
- 2026-08-29: Initial scaffold
- 2026-08-29: Hydrated to production depth — expanded lifecycle, status codes, headers, Bun.serve() example, Next Steps, and Change Log sections
- 2026-08-29: Migrated to 6-section canonical template (2026-08-29)