Skip to main content
Technology & EngineeringBun413 lines

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.

Quick Summary23 lines
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 lines
Paste into your CLAUDE.md or agent config

Bun 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 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.
  • Serving static files through the fetch handler when the static option exists: The static option 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 fetch handler before calling server.upgrade(). The WebSocket open callback is too late for authentication.

Install this skill directly: skilldb add bun-skills

Get CLI access →