Bun HTTP Server
Building HTTP servers with Bun: Bun.serve() API, routing patterns, WebSocket support, streaming responses, static file serving, TLS configuration, hot reloading, and integration with frameworks like Hono and Elysia.
You are an expert in building HTTP servers with Bun, covering the native Bun.serve() API, WebSocket upgrades, streaming, and production deployment patterns. ## Key Points - **Blocking the fetch handler with synchronous I/O**: Long-running synchronous operations block all other requests. Use async APIs or offload to worker threads. - **Not handling the error callback**: Without an `error` handler, uncaught errors in `fetch` crash the server. Always provide an error handler that returns a 500 response. - **Using Express with Bun**: Express works on Bun but does not leverage Bun.serve() performance. Use Hono or Elysia for Bun-optimized frameworks, or use Bun.serve() directly. ## Quick Example ```bash openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost" ``` ```bash # Restart server on file changes bun --watch run server.ts # Hot reload (preserves state, reloads modules) bun --hot run server.ts ```
skilldb get bun-skills/Bun HTTP ServerFull skill: 413 linesBun HTTP Server — High-Performance Web Servers
You are an expert in building HTTP servers with Bun, covering the native Bun.serve() API, WebSocket upgrades, streaming, and production deployment patterns.
Bun.serve() Basics
Bun's HTTP server is built on a single function: Bun.serve(). It returns a Server object and begins listening immediately:
const server = Bun.serve({
port: 3000,
hostname: "0.0.0.0", // default: "0.0.0.0"
fetch(req: Request): Response | Promise<Response> {
return new Response("Hello, World!");
},
error(err: Error): Response {
console.error(err);
return new Response("Internal Server Error", { status: 500 });
},
});
console.log(`Server running at ${server.url}`);
The fetch handler receives a standard Request object and must return a Response (or a Promise<Response>). This follows the Web Fetch API standard, making your server code portable to other runtimes like Cloudflare Workers and Deno.
Routing Patterns
Bun.serve() does not include a built-in router. Route manually using URL parsing:
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
const path = url.pathname;
const method = req.method;
// Static routes
if (method === "GET" && path === "/") {
return new Response("Home page");
}
if (method === "GET" && path === "/api/health") {
return Response.json({ status: "ok", uptime: process.uptime() });
}
// Dynamic routes with pattern matching
const userMatch = path.match(/^\/api\/users\/(\d+)$/);
if (method === "GET" && userMatch) {
const userId = userMatch[1];
return Response.json({ id: userId, name: "Alice" });
}
// POST with JSON body
if (method === "POST" && path === "/api/users") {
const body = await req.json();
return Response.json({ id: 1, ...body }, { status: 201 });
}
// 404
return new Response("Not Found", { status: 404 });
},
});
Route Table Pattern
For cleaner organization, use a route table:
type Handler = (req: Request, params: Record<string, string>) => Response | Promise<Response>;
interface Route {
method: string;
pattern: RegExp;
keys: string[];
handler: Handler;
}
const routes: Route[] = [];
function addRoute(method: string, path: string, handler: Handler) {
const keys: string[] = [];
const pattern = new RegExp(
"^" + path.replace(/:(\w+)/g, (_, key) => {
keys.push(key);
return "([^/]+)";
}) + "$"
);
routes.push({ method, pattern, keys, handler });
}
addRoute("GET", "/api/users/:id", (req, params) => {
return Response.json({ userId: params.id });
});
addRoute("POST", "/api/users", async (req) => {
const body = await req.json();
return Response.json(body, { status: 201 });
});
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
for (const route of routes) {
if (req.method !== route.method) continue;
const match = url.pathname.match(route.pattern);
if (!match) continue;
const params: Record<string, string> = {};
route.keys.forEach((key, i) => params[key] = match[i + 1]);
return route.handler(req, params);
}
return new Response("Not Found", { status: 404 });
},
});
Request Handling
Bun.serve({
port: 3000,
async fetch(req) {
// Headers
const auth = req.headers.get("Authorization");
const contentType = req.headers.get("Content-Type");
// Query parameters
const url = new URL(req.url);
const page = url.searchParams.get("page") ?? "1";
// Body parsing
const jsonBody = await req.json(); // parse JSON
const textBody = await req.text(); // raw text
const formData = await req.formData(); // form data
const blob = await req.blob(); // binary data
const buffer = await req.arrayBuffer(); // ArrayBuffer
// Response with headers
return new Response("OK", {
status: 200,
headers: {
"Content-Type": "text/plain",
"X-Request-Id": crypto.randomUUID(),
"Cache-Control": "no-cache",
},
});
},
});
Static File Serving
Bun.serve() has built-in optimized static file serving:
Bun.serve({
port: 3000,
// Serve static files from a directory
static: {
"/": new Response(await Bun.file("public/index.html").bytes(), {
headers: { "Content-Type": "text/html" },
}),
"/style.css": new Response(await Bun.file("public/style.css").bytes(), {
headers: { "Content-Type": "text/css" },
}),
},
fetch(req) {
// Dynamic routes handled here
// Static routes above are matched first (faster)
return new Response("Not Found", { status: 404 });
},
});
For dynamic file serving:
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
// Serve files from public directory
if (url.pathname.startsWith("/static/")) {
const filePath = `./public${url.pathname.replace("/static", "")}`;
const file = Bun.file(filePath);
if (await file.exists()) {
return new Response(file);
}
return new Response("Not Found", { status: 404 });
}
return new Response("API");
},
});
Streaming Responses
Bun.serve({
port: 3000,
fetch(req) {
// Stream a large file
const file = Bun.file("large-dataset.csv");
return new Response(file.stream());
},
});
Server-Sent Events
Bun.serve({
port: 3000,
fetch(req) {
if (new URL(req.url).pathname === "/events") {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
let id = 0;
const interval = setInterval(() => {
const event = `id: ${id++}\nevent: update\ndata: ${JSON.stringify({ time: Date.now() })}\n\n`;
controller.enqueue(encoder.encode(event));
}, 1000);
req.signal.addEventListener("abort", () => {
clearInterval(interval);
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
}
return new Response("Not Found", { status: 404 });
},
});
WebSocket Support
Bun has first-class WebSocket support built into Bun.serve():
type WebSocketData = { userId: string; joinedAt: number };
Bun.serve<WebSocketData>({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/ws") {
const userId = url.searchParams.get("userId") ?? "anonymous";
const upgraded = server.upgrade(req, {
data: { userId, joinedAt: Date.now() },
});
if (!upgraded) {
return new Response("Upgrade failed", { status: 400 });
}
return undefined; // Bun handles the response for upgraded requests
}
return new Response("Hello");
},
websocket: {
open(ws) {
console.log(`${ws.data.userId} connected`);
ws.subscribe("chat"); // subscribe to a pub/sub topic
},
message(ws, message) {
// Broadcast to all subscribers
ws.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws, code, reason) {
console.log(`${ws.data.userId} disconnected: ${code}`);
ws.unsubscribe("chat");
},
drain(ws) {
// Called when backpressure is relieved
},
// Configuration
maxPayloadLength: 16 * 1024 * 1024, // 16 MB
idleTimeout: 120, // seconds
backpressureLimit: 1024 * 1024, // 1 MB
perMessageDeflate: true, // compression
},
});
The pub/sub system (ws.subscribe, ws.publish) is built into Bun and operates in-process without an external message broker. For multi-process setups, use Redis or NATS for cross-process messaging.
TLS / HTTPS
Bun.serve({
port: 443,
tls: {
cert: Bun.file("./certs/cert.pem"),
key: Bun.file("./certs/key.pem"),
// ca: Bun.file("./certs/ca.pem"), // optional CA cert
},
fetch(req) {
return new Response("Secure!");
},
});
For local development, generate self-signed certs:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
Hot Reloading
# Restart server on file changes
bun --watch run server.ts
# Hot reload (preserves state, reloads modules)
bun --hot run server.ts
With --hot, the fetch handler is replaced in-place when files change. Open connections are not dropped, and global state is preserved. This is ideal for development.
Frameworks: Hono
Hono is a lightweight, fast web framework that works natively with Bun:
bun add hono
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
const app = new Hono();
app.use("*", logger());
app.use("/api/*", cors());
app.get("/", (c) => c.text("Hello Hono!"));
app.get("/api/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ id, name: "Alice" });
});
app.post("/api/users", async (c) => {
const body = await c.req.json();
return c.json(body, 201);
});
export default app; // Bun auto-detects the default export with a fetch method
Frameworks: Elysia
Elysia is a Bun-first framework with end-to-end type safety:
bun add elysia
import { Elysia, t } from "elysia";
const app = new Elysia()
.get("/", () => "Hello Elysia!")
.get("/api/users/:id", ({ params: { id } }) => ({ id, name: "Alice" }))
.post("/api/users", ({ body }) => ({ id: 1, ...body }), {
body: t.Object({
name: t.String(),
email: t.String({ format: "email" }),
}),
})
.listen(3000);
console.log(`Running at ${app.server?.url}`);
Anti-Patterns
- Creating a new URL object for every request without need:
new URL(req.url)allocates memory. If you only need the pathname, consider a simpler string operation for hot paths, though for most apps the URL constructor is fine. - Blocking the fetch handler with synchronous I/O: Long-running synchronous operations block all other requests. Use async APIs or offload to worker threads.
- Not handling the error callback: Without an
errorhandler, uncaught errors infetchcrash the server. Always provide an error handler that returns a 500 response. - Using Express with Bun: Express works on Bun but does not leverage Bun.serve() performance. Use Hono or Elysia for Bun-optimized frameworks, or use Bun.serve() directly.
- Serving static files through the fetch handler when the static option exists: The
staticoption in Bun.serve() is optimized with caching and bypasses JavaScript entirely for matched routes. Use it for known static assets. - Opening WebSocket connections without authentication: Always validate auth tokens in the
fetchhandler before callingserver.upgrade(). The WebSocketopencallback is too late for authentication.
Install this skill directly: skilldb add bun-skills
Related Skills
Bun Bundler
Bun's built-in bundler: Bun.build() API, entry points, output formats (esm, cjs, iife), plugins, loaders, tree shaking, code splitting, CSS bundling, HTML entries, and compile-time macros.
Bun Fundamentals
Bun runtime overview: all-in-one JavaScript runtime, bundler, test runner, and package manager. Installation, project initialization, Node.js compatibility, performance characteristics, and guidance on when to choose Bun vs Node.
Bun Node.js Migration
Migrating from Node.js to Bun: compatibility checklist, node:* module imports, native addon handling, environment variable differences, Docker setup, CI/CD pipeline changes, and common migration pitfalls.
Bun Package Manager
Bun as a package manager: bun install, bun add, bun remove, the binary lockfile (bun.lockb), workspace support, overrides, patching, publishing packages, global cache, and comparison to npm, pnpm, and yarn.
Bun Production Patterns
Production patterns for Bun: Docker deployments, TypeScript configuration, shell scripting with Bun.$, monorepo setup, database access patterns, and deployment to Fly.io and Railway.
Bun Runtime APIs
Bun-native runtime APIs including Bun.serve(), Bun.file(), Bun.write(), Bun.spawn(), Bun.sleep(), Bun.env, FFI for calling native libraries, built-in SQLite, S3 client, glob, and semver utilities.