> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/oven-sh/bun/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP Server

> Start a high-performance HTTP server with Bun.serve(). Supports routing, streaming, TLS, WebSockets, and more.

Bun ships a built-in HTTP server powered by [uWebSockets](https://github.com/uNetworking/uWebSockets). It handles \~160,000 requests/second on Linux — roughly 2.5x Node.js — with zero dependencies.

## Basic server

```typescript theme={null}
const server = Bun.serve({
  port: 3000,
  fetch(req: Request): Response {
    return new Response("Hello!");
  },
});

console.log(`Listening on ${server.url}`);
```

The `fetch` handler receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and must return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) or `Promise<Response>`.

<Note>
  Port defaults to `$BUN_PORT`, `$PORT`, or `$NODE_PORT` when the `port` option is omitted. You can also set it via `bun --port=4000 server.ts`.
</Note>

***

## Routing

Use the `routes` option (requires Bun v1.2.3+) to define path-based handlers with static paths, route parameters, and wildcards.

```typescript theme={null}
const server = Bun.serve({
  routes: {
    // Static routes
    "/api/status": new Response("OK"),

    // Dynamic route parameters
    "/users/:id": req => {
      return new Response(`Hello, user ${req.params.id}!`);
    },

    // Per-method handlers
    "/api/posts": {
      GET: () => new Response("List posts"),
      POST: async req => {
        const body = await req.json();
        return Response.json({ created: true, ...body });
      },
    },

    // Wildcard catch-all
    "/api/*": Response.json({ message: "Not found" }, { status: 404 }),

    // Redirect
    "/blog/hello": Response.redirect("/blog/hello/world"),

    // Serve a file
    "/favicon.ico": Bun.file("./favicon.ico"),
  },

  // Fallback for routes not matched above
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});
```

### Route precedence

Routes are matched from most to least specific:

1. Exact routes (`/users/all`)
2. Parameter routes (`/users/:id`)
3. Wildcard routes (`/users/*`)
4. Global catch-all (`/*`)

### Type-safe route parameters

TypeScript infers parameter names directly from the route string literal:

```typescript theme={null}
import type { BunRequest } from "bun";

Bun.serve({
  routes: {
    "/orgs/:orgId/repos/:repoId": req => {
      const { orgId, repoId } = req.params; // fully typed
      return Response.json({ orgId, repoId });
    },
  },
});
```

### Query parameters

Parse query parameters from the request URL:

```typescript theme={null}
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    const name = url.searchParams.get("name") ?? "world";
    return new Response(`Hello, ${name}!`);
  },
});
```

***

## Reading request bodies

<CodeGroup>
  ```typescript JSON body theme={null}
  Bun.serve({
    routes: {
      "/api/echo": {
        POST: async req => {
          const data = await req.json();
          return Response.json(data);
        },
      },
    },
  });
  ```

  ```typescript Text body theme={null}
  Bun.serve({
    routes: {
      "/api/text": {
        POST: async req => {
          const text = await req.text();
          return new Response(`Received: ${text}`);
        },
      },
    },
  });
  ```

  ```typescript FormData body theme={null}
  Bun.serve({
    routes: {
      "/api/upload": {
        POST: async req => {
          const form = await req.formData();
          const file = form.get("file") as File;
          return Response.json({ name: file.name, size: file.size });
        },
      },
    },
  });
  ```
</CodeGroup>

***

## JSON responses

Use `Response.json()` to serialize data with the correct `Content-Type` header:

```typescript theme={null}
Bun.serve({
  routes: {
    "/api/user": () =>
      Response.json({
        id: 1,
        name: "Alice",
        role: "admin",
      }),
  },
});
```

***

## Streaming responses

Return a `ReadableStream` or an async generator to stream data to the client:

<CodeGroup>
  ```typescript ReadableStream theme={null}
  Bun.serve({
    fetch(req) {
      const stream = new ReadableStream({
        async start(controller) {
          controller.enqueue("Hello");
          await Bun.sleep(500);
          controller.enqueue(", world!");
          controller.close();
        },
      });

      return new Response(stream, {
        headers: { "Content-Type": "text/plain" },
      });
    },
  });
  ```

  ```typescript Async generator theme={null}
  Bun.serve({
    routes: {
      "/events": (req, server) => {
        // Disable idle timeout so the stream stays open
        server.timeout(req, 0);

        return new Response(
          async function* () {
            yield "data: hello\n\n";
            await Bun.sleep(1000);
            yield "data: world\n\n";
          },
          { headers: { "Content-Type": "text/event-stream" } },
        );
      },
    },
  });
  ```
</CodeGroup>

<Warning>
  The idle timeout (default: 10 seconds) applies to streaming responses. If your stream goes quiet for longer than `idleTimeout`, the connection closes mid-stream. Disable it per-request with `server.timeout(req, 0)`.
</Warning>

***

## Static file serving

Return a `BunFile` as the response body to serve files from disk. Bun uses the `sendfile(2)` syscall for zero-copy transfers when possible.

```typescript theme={null}
Bun.serve({
  routes: {
    // Served from filesystem on every request; supports range requests and 304s
    "/download/:file": req => {
      return new Response(Bun.file(`./public/${req.params.file}`));
    },

    // Buffered in memory at startup; zero allocation per request
    "/logo.png": new Response(await Bun.file("./public/logo.png").bytes()),
  },
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});
```

***

## TLS / HTTPS

Enable HTTPS by providing `key` and `cert` in the `tls` option. Bun uses [BoringSSL](https://boringssl.googlesource.com/boringssl) under the hood.

```typescript theme={null}
Bun.serve({
  port: 443,
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },
  fetch(req) {
    return new Response("Secure!");
  },
});
```

`key` and `cert` accept a string, `BunFile`, `Buffer`, `TypedArray`, or an array of any of those.

<AccordionGroup>
  <Accordion title="Encrypted private key">
    ```typescript theme={null}
    Bun.serve({
      tls: {
        key: Bun.file("./key.pem"),
        cert: Bun.file("./cert.pem"),
        passphrase: "my-secret-passphrase",
      },
      fetch(req) {
        return new Response("OK");
      },
    });
    ```
  </Accordion>

  <Accordion title="Custom CA certificate">
    ```typescript theme={null}
    Bun.serve({
      tls: {
        key: Bun.file("./key.pem"),
        cert: Bun.file("./cert.pem"),
        ca: Bun.file("./ca.pem"),
      },
      fetch(req) {
        return new Response("OK");
      },
    });
    ```
  </Accordion>

  <Accordion title="Multiple server names (SNI)">
    ```typescript theme={null}
    Bun.serve({
      tls: [
        {
          key: Bun.file("./key1.pem"),
          cert: Bun.file("./cert1.pem"),
          serverName: "api.example.com",
        },
        {
          key: Bun.file("./key2.pem"),
          cert: Bun.file("./cert2.pem"),
          serverName: "www.example.com",
        },
      ],
      fetch(req) {
        return new Response("OK");
      },
    });
    ```
  </Accordion>
</AccordionGroup>

***

## Error handling

Use the `error` handler to catch unhandled errors thrown inside `fetch` or route handlers and return a custom `Response`:

```typescript theme={null}
Bun.serve({
  fetch(req) {
    throw new Error("Something went wrong");
  },
  error(error) {
    console.error(error);
    return new Response(`Internal Server Error: ${error.message}`, {
      status: 500,
      headers: { "Content-Type": "text/plain" },
    });
  },
});
```

Enable `development: true` to display a full error page with stack traces in the browser during development:

```typescript theme={null}
Bun.serve({
  development: true,
  fetch(req) {
    throw new Error("woops!");
  },
});
```

***

## Server lifecycle

### `server.stop()`

Stop the server from accepting new connections. By default it waits for in-flight requests to finish; pass `true` to close all connections immediately.

```typescript theme={null}
const server = Bun.serve({
  fetch(req) {
    return new Response("Hello!");
  },
});

// Graceful shutdown
await server.stop();

// Force shutdown
await server.stop(true);
```

### `server.reload()`

Hot-swap `fetch`, `error`, and `routes` handlers without restarting the process or dropping connections:

```typescript theme={null}
const server = Bun.serve({
  routes: {
    "/api/version": () => Response.json({ version: "1.0.0" }),
  },
  fetch(req) {
    return new Response("v1");
  },
});

server.reload({
  routes: {
    "/api/version": () => Response.json({ version: "2.0.0" }),
  },
  fetch(req) {
    return new Response("v2");
  },
});
```

***

## Per-request controls

### `server.timeout(req, seconds)`

Override the idle timeout for a single request. Pass `0` to disable it entirely — useful for Server-Sent Events and long-running streams.

```typescript theme={null}
Bun.serve({
  async fetch(req, server) {
    server.timeout(req, 60); // allow 60 s of inactivity for this request
    const body = await req.text();
    return new Response(`Got: ${body}`);
  },
});
```

### `server.requestIP(req)`

Get the client's IP address and port:

```typescript theme={null}
Bun.serve({
  fetch(req, server) {
    const addr = server.requestIP(req);
    return new Response(`Client: ${addr?.address}:${addr?.port}`);
  },
});
```

Returns `null` for Unix socket connections or closed requests.

***

## Unix domain sockets

Listen on a Unix socket instead of a TCP port:

```typescript theme={null}
Bun.serve({
  unix: "/tmp/my-app.sock",
  fetch(req) {
    return new Response("Hello from Unix socket!");
  },
});
```

***

## `export default` syntax

Instead of calling `Bun.serve()` explicitly, you can `export default` a server config object. Bun detects the `fetch` property and starts the server automatically:

```typescript theme={null}
import type { Serve } from "bun";

export default {
  port: 3000,
  fetch(req) {
    return new Response("Bun!");
  },
} satisfies Serve.Options<undefined>;
```

***

## Practical example: REST API

<CodeGroup>
  ```typescript server.ts theme={null}
  import { Database } from "bun:sqlite";

  const db = new Database("posts.db");
  db.exec(`
    CREATE TABLE IF NOT EXISTS posts (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      content TEXT NOT NULL,
      created_at TEXT NOT NULL
    )
  `);

  Bun.serve({
    routes: {
      "/api/posts": {
        GET: () => {
          const posts = db.query("SELECT * FROM posts").all();
          return Response.json(posts);
        },
        POST: async req => {
          const body = await req.json();
          const id = crypto.randomUUID();
          db.query(
            "INSERT INTO posts (id, title, content, created_at) VALUES (?, ?, ?, ?)",
          ).run(id, body.title, body.content, new Date().toISOString());
          return Response.json({ id, ...body }, { status: 201 });
        },
      },
      "/api/posts/:id": req => {
        const post = db
          .query("SELECT * FROM posts WHERE id = ?")
          .get(req.params.id);
        if (!post) return new Response("Not Found", { status: 404 });
        return Response.json(post);
      },
    },
    error(error) {
      console.error(error);
      return new Response("Internal Server Error", { status: 500 });
    },
  });
  ```

  ```typescript types.ts theme={null}
  export interface Post {
    id: string;
    title: string;
    content: string;
    created_at: string;
  }
  ```
</CodeGroup>

***

## Reference

```typescript theme={null}
interface Server extends Disposable {
  stop(closeActiveConnections?: boolean): Promise<void>;
  reload(options: Serve): void;
  fetch(request: Request | string): Response | Promise<Response>;
  upgrade<T = undefined>(
    request: Request,
    options?: { headers?: Bun.HeadersInit; data?: T },
  ): boolean;
  publish(
    topic: string,
    data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
    compress?: boolean,
  ): ServerWebSocketSendStatus;
  subscriberCount(topic: string): number;
  requestIP(request: Request): SocketAddress | null;
  timeout(request: Request, seconds: number): void;
  ref(): void;
  unref(): void;
  readonly pendingRequests: number;
  readonly pendingWebSockets: number;
  readonly url: URL;
  readonly port: number;
  readonly hostname: string;
  readonly development: boolean;
  readonly id: string;
}
```
