Skip to content

Bun HTTP Server

Bun has a built-in high-performance HTTP server that allows creating web services with a concise API. This chapter introduces Bun HTTP server usage.

Basic Server

Minimal Example

typescript
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello, Bun!");
  },
});

console.log(`Server running at http://localhost:${server.port}`);

Server Configuration

typescript
const server = Bun.serve({
  // Port number
  port: 3000,
  
  // Hostname
  hostname: "127.0.0.1",
  
  // Request handler
  fetch(request, server) {
    return new Response("Hello!");
  },
  
  // Error handler
  error(error) {
    console.error("Server error:", error);
    return new Response("Server error", { status: 500 });
  },
});

Request Handling

Request Object

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    // Request method
    console.log("Method:", request.method);
    
    // Request URL
    const url = new URL(request.url);
    console.log("Path:", url.pathname);
    console.log("Query params:", url.searchParams);
    
    // Request headers
    console.log("Content-Type:", request.headers.get("content-type"));
    console.log("User-Agent:", request.headers.get("user-agent"));
    
    return new Response("OK");
  },
});

Getting Request Body

typescript
Bun.serve({
  port: 3000,
  async fetch(request) {
    // JSON body
    if (request.headers.get("content-type")?.includes("application/json")) {
      const body = await request.json();
      console.log("JSON:", body);
    }
    
    // Text body
    const text = await request.text();
    
    // FormData
    const formData = await request.formData();
    
    // ArrayBuffer
    const buffer = await request.arrayBuffer();
    
    return new Response("Request received");
  },
});

Form Data

typescript
Bun.serve({
  port: 3000,
  async fetch(request) {
    if (request.method === "POST") {
      const formData = await request.formData();
      
      // Get fields
      const name = formData.get("name");
      const email = formData.get("email");
      
      // Get file
      const file = formData.get("avatar") as File;
      if (file) {
        console.log("File name:", file.name);
        console.log("File size:", file.size);
        
        // Save file
        await Bun.write(`./uploads/${file.name}`, file);
      }
      
      return Response.json({ name, email });
    }
    
    return new Response("Please use POST request");
  },
});

Response Handling

Response Object

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    // Text response
    return new Response("Hello, World!");
    
    // With status code
    return new Response("Not found", { status: 404 });
    
    // With headers
    return new Response("Hello", {
      status: 200,
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "X-Custom-Header": "custom-value",
      },
    });
  },
});

JSON Response

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    const data = {
      message: "Success",
      timestamp: new Date().toISOString(),
      data: { id: 1, name: "Bun" },
    };
    
    // Use Response.json()
    return Response.json(data);
    
    // With status code
    return Response.json({ error: "Not found" }, { status: 404 });
    
    // Manual creation
    return new Response(JSON.stringify(data), {
      headers: { "Content-Type": "application/json" },
    });
  },
});

HTML Response

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    const html = `
      <!DOCTYPE html>
      <html>
        <head><title>Bun Server</title></head>
        <body>
          <h1>Welcome to Bun!</h1>
          <p>Current time: ${new Date().toLocaleString()}</p>
        </body>
      </html>
    `;
    
    return new Response(html, {
      headers: { "Content-Type": "text/html; charset=utf-8" },
    });
  },
});

File Response

typescript
Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    
    // Return static file
    if (url.pathname === "/image.png") {
      const file = Bun.file("./public/image.png");
      return new Response(file);
    }
    
    // Set download filename
    if (url.pathname === "/download") {
      const file = Bun.file("./files/document.pdf");
      return new Response(file, {
        headers: {
          "Content-Disposition": 'attachment; filename="document.pdf"',
        },
      });
    }
    
    return new Response("Not Found", { status: 404 });
  },
});

Routing

Simple Routing

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;
    
    // Home page
    if (path === "/" && method === "GET") {
      return new Response("Home");
    }
    
    // API routes
    if (path === "/api/users" && method === "GET") {
      return Response.json([{ id: 1, name: "John" }]);
    }
    
    if (path === "/api/users" && method === "POST") {
      return Response.json({ message: "User created" }, { status: 201 });
    }
    
    // 404
    return new Response("Not Found", { status: 404 });
  },
});

Route Parameters

typescript
Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname;
    
    // Match /api/users/:id
    const userMatch = path.match(/^\/api\/users\/(\d+)$/);
    if (userMatch) {
      const userId = userMatch[1];
      return Response.json({ id: userId, name: `User ${userId}` });
    }
    
    // Match /api/posts/:id/comments/:commentId
    const commentMatch = path.match(/^\/api\/posts\/(\d+)\/comments\/(\d+)$/);
    if (commentMatch) {
      const [, postId, commentId] = commentMatch;
      return Response.json({ postId, commentId });
    }
    
    return new Response("Not Found", { status: 404 });
  },
});

Router Class

typescript
// router.ts
type Handler = (request: Request, params: Record<string, string>) => Response | Promise<Response>;

interface Route {
  method: string;
  pattern: RegExp;
  paramNames: string[];
  handler: Handler;
}

class Router {
  private routes: Route[] = [];

  private pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
    const paramNames: string[] = [];
    const pattern = path.replace(/:(\w+)/g, (_, name) => {
      paramNames.push(name);
      return "([^/]+)";
    });
    return { pattern: new RegExp(`^${pattern}$`), paramNames };
  }

  add(method: string, path: string, handler: Handler) {
    const { pattern, paramNames } = this.pathToRegex(path);
    this.routes.push({ method, pattern, paramNames, handler });
  }

  get(path: string, handler: Handler) { this.add("GET", path, handler); }
  post(path: string, handler: Handler) { this.add("POST", path, handler); }
  put(path: string, handler: Handler) { this.add("PUT", path, handler); }
  delete(path: string, handler: Handler) { this.add("DELETE", path, handler); }

  async handle(request: Request): Promise<Response> {
    const url = new URL(request.url);
    
    for (const route of this.routes) {
      if (route.method !== request.method) continue;
      
      const match = url.pathname.match(route.pattern);
      if (match) {
        const params: Record<string, string> = {};
        route.paramNames.forEach((name, i) => {
          params[name] = match[i + 1];
        });
        return route.handler(request, params);
      }
    }
    
    return new Response("Not Found", { status: 404 });
  }
}

// Usage
const router = new Router();

router.get("/", () => new Response("Home"));
router.get("/api/users", () => Response.json([{ id: 1 }]));
router.get("/api/users/:id", (req, params) => 
  Response.json({ id: params.id })
);
router.post("/api/users", async (req) => {
  const body = await req.json();
  return Response.json({ created: body }, { status: 201 });
});

Bun.serve({
  port: 3000,
  fetch: (req) => router.handle(req),
});

Middleware Pattern

Implementing Middleware

typescript
type Middleware = (
  request: Request,
  next: () => Promise<Response>
) => Promise<Response>;

function compose(middlewares: Middleware[]) {
  return async (request: Request): Promise<Response> => {
    let index = 0;
    
    async function next(): Promise<Response> {
      if (index >= middlewares.length) {
        return new Response("Not Found", { status: 404 });
      }
      const middleware = middlewares[index++];
      return middleware(request, next);
    }
    
    return next();
  };
}

// Logger middleware
const logger: Middleware = async (req, next) => {
  const start = Date.now();
  const response = await next();
  console.log(`${req.method} ${new URL(req.url).pathname} - ${Date.now() - start}ms`);
  return response;
};

// CORS middleware
const cors: Middleware = async (req, next) => {
  const response = await next();
  response.headers.set("Access-Control-Allow-Origin", "*");
  response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  return response;
};

// Handler middleware
const handler: Middleware = async (req, next) => {
  return new Response("Hello!");
};

// Compose and use
const app = compose([logger, cors, handler]);

Bun.serve({
  port: 3000,
  fetch: app,
});

Static File Serving

Simple Static Server

typescript
import path from "node:path";

const PUBLIC_DIR = "./public";

Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    let filePath = path.join(PUBLIC_DIR, url.pathname);
    
    // Directory default returns index.html
    if (filePath.endsWith("/")) {
      filePath += "index.html";
    }
    
    const file = Bun.file(filePath);
    
    if (await file.exists()) {
      return new Response(file);
    }
    
    return new Response("Not Found", { status: 404 });
  },
});

MIME Type Handling

typescript
const MIME_TYPES: Record<string, string> = {
  ".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",
  ".woff": "font/woff",
  ".woff2": "font/woff2",
};

function getMimeType(filePath: string): string {
  const ext = path.extname(filePath).toLowerCase();
  return MIME_TYPES[ext] || "application/octet-stream";
}

Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    const filePath = path.join("./public", url.pathname);
    const file = Bun.file(filePath);
    
    if (await file.exists()) {
      return new Response(file, {
        headers: { "Content-Type": getMimeType(filePath) },
      });
    }
    
    return new Response("Not Found", { status: 404 });
  },
});

HTTPS Server

TLS Configuration

typescript
Bun.serve({
  port: 443,
  
  // TLS certificates
  tls: {
    key: Bun.file("./certs/key.pem"),
    cert: Bun.file("./certs/cert.pem"),
    
    // Optional: CA certificate
    // ca: Bun.file("./certs/ca.pem"),
    
    // Optional: passphrase
    // passphrase: "your-passphrase",
  },
  
  fetch(request) {
    return new Response("Secure connection!");
  },
});

Self-signed Certificate

bash
# Generate self-signed certificate (for development)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

Server Control

Stopping Server

typescript
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello!");
  },
});

// Stop server
server.stop();

// Or stop immediately
server.stop(true);

Reload

typescript
// Hot reload support
export default {
  port: 3000,
  fetch(request: Request) {
    return new Response("Hello!");
  },
};

// Run: bun --hot server.ts

Getting Server Info

typescript
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello!");
  },
});

console.log("Port:", server.port);
console.log("Hostname:", server.hostname);
console.log("Development:", server.development);
console.log("Pending requests:", server.pendingRequests);

Complete REST API Example

typescript
// api-server.ts
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: "John", email: "john@example.com" },
  { id: 2, name: "Jane", email: "jane@example.com" },
];

let nextId = 3;

const server = Bun.serve({
  port: 3000,
  
  async fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;
    
    // CORS
    if (method === "OPTIONS") {
      return new Response(null, {
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
          "Access-Control-Allow-Headers": "Content-Type",
        },
      });
    }
    
    // GET /api/users
    if (path === "/api/users" && method === "GET") {
      return Response.json(users);
    }
    
    // GET /api/users/:id
    const getMatch = path.match(/^\/api\/users\/(\d+)$/);
    if (getMatch && method === "GET") {
      const user = users.find(u => u.id === parseInt(getMatch[1]));
      if (user) {
        return Response.json(user);
      }
      return Response.json({ error: "User not found" }, { status: 404 });
    }
    
    // POST /api/users
    if (path === "/api/users" && method === "POST") {
      const body = await request.json();
      const newUser: User = {
        id: nextId++,
        name: body.name,
        email: body.email,
      };
      users.push(newUser);
      return Response.json(newUser, { status: 201 });
    }
    
    // PUT /api/users/:id
    const putMatch = path.match(/^\/api\/users\/(\d+)$/);
    if (putMatch && method === "PUT") {
      const index = users.findIndex(u => u.id === parseInt(putMatch[1]));
      if (index !== -1) {
        const body = await request.json();
        users[index] = { ...users[index], ...body };
        return Response.json(users[index]);
      }
      return Response.json({ error: "User not found" }, { status: 404 });
    }
    
    // DELETE /api/users/:id
    const deleteMatch = path.match(/^\/api\/users\/(\d+)$/);
    if (deleteMatch && method === "DELETE") {
      const index = users.findIndex(u => u.id === parseInt(deleteMatch[1]));
      if (index !== -1) {
        users.splice(index, 1);
        return new Response(null, { status: 204 });
      }
      return Response.json({ error: "User not found" }, { status: 404 });
    }
    
    return Response.json({ error: "Not found" }, { status: 404 });
  },
  
  error(error) {
    console.error(error);
    return Response.json({ error: "Server error" }, { status: 500 });
  },
});

console.log(`API server running at http://localhost:${server.port}`);

Summary

This chapter covered:

  • ✅ Creating basic HTTP server
  • ✅ Handling requests and responses
  • ✅ Routing and parameter parsing
  • ✅ Middleware pattern
  • ✅ Static file serving
  • ✅ HTTPS configuration
  • ✅ Complete REST API example

Next Steps

Continue reading WebSocket to learn about Bun's real-time communication features.

Content is for learning and research only.