Skip to main content
skill CX Codex

bun-http-server — Claude Code skill: HTTP servers on Bun

A skill that tells the agent how to stand up an HTTP server with Bun.serve(): routes, body parsing, responses, CORS.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add secondsky/claude-skills --skill bun-http-server -a claude-code
Open on GitHub

What bun-http-server does

The skill kicks in when you ask the agent to write a Bun server, add a route, or parse a request body. It ships ready-made patterns: a basic Bun.serve() with a port and a fetch handler, reading the method, path and query params through new URL(req.url), and parsing JSON, form data, text and ArrayBuffer.

Routing gets its own section. The classic fetch(req) handler with manual URL parsing still works, but as of Bun 1.2 the canonical way is the routes option, with dynamic params on req.params. The skill advises using routes for new code and keeping fetch as a fallback for unmatched paths.

There are also sections on static files via Bun.file(), CORS with preflight handling, a global error handler, and a table of common failures like EADDRINUSE or 413 Payload Too Large. Two things are worth remembering. A request body can only be read once, so a second read throws. And server.stop() returns a promise in Bun 1.2 and later, so calling it without await may leave in-flight connections undrained.

What it helps with

  • stand up an HTTP server on Bun from scratch
  • add a route with a dynamic path parameter
  • parse JSON or form data from a request body
  • serve a static file or a stream in the response
  • set up CORS and handle preflight requests

Example prompts for the agent

  • Write a Bun.serve server with an /api/users route returning JSON
  • Add an /api/users/:id route to this server using req.params
  • Why does reading the request body a second time fail, and how do I fix it?

How to install bun-http-server

1Skills CLI (Vercel)

npx skills add secondsky/claude-skills --skill bun-http-server -a claude-code

2Manually: put the files in place

~/.claude/skills/bun-http-server/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/bun-http-server/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about bun-http-server

How do I install bun-http-server in Claude?

Run npx skills add secondsky/claude-skills --skill bun-http-server -a claude-code. The skill file then lives at ~/.claude/skills/bun-http-server/SKILL.md.

What does bun-http-server do?

It gives the agent patterns for Bun HTTP servers: a basic Bun.serve(), request and body parsing, response types, routing, static files and CORS.

How is routes different from fetch in Bun.serve?

The routes option arrived in Bun 1.2 and gives you a route map with params on req.params. fetch stays as the fallback handler for paths that match no route.

Source text

SKILL.md · MIT
---
name: bun-http-server
description: Use when building HTTP servers with Bun.serve, handling requests/responses, implementing routing, creating REST APIs, or configuring fetch handlers.
metadata:
  version: "1.0.0"
license: MIT
---

# Bun HTTP Server

Bun has a built-in high-performance HTTP server via `Bun.serve()`.

## Quick Start

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

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

## Request Handling

```typescript
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);

    // Method
    console.log(req.method); // GET, POST, etc.

    // Path
    console.log(url.pathname); // /api/users

    // Query params
    console.log(url.searchParams.get("id")); // ?id=123

    // Headers
    console.log(req.headers.get("Content-Type"));

    return new Response("OK");
  },
});
```

## Body Parsing

```typescript
Bun.serve({
  async fetch(req) {
    // JSON
    const json = await req.json();

    // Form data
    const form = await req.formData();

    // Text
    const text = await req.text();

    // ArrayBuffer
    const buffer = await req.arrayBuffer();

    // Blob
    const blob = await req.blob();

    return new Response("Received");
  },
});
```

## Response Types

```typescript
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);

    switch (url.pathname) {
      case "/json":
        return Response.json({ message: "Hello" });

      case "/html":
        return new Response("<h1>Hello</h1>", {
          headers: { "Content-Type": "text/html" },
        });

      case "/redirect":
        return Response.redirect("/new-location", 302);

      case "/file":
        return new Response(Bun.file("./image.png"));

      case "/stream":
        return new Response(
          new ReadableStream({
            start(controller) {
              controller.enqueue("chunk1");
              controller.enqueue("chunk2");
              controller.close();
            },
          })
        );

      default:
        return new Response("Not Found", { status: 404 });
    }
  },
});
```

## Simple Routing

```typescript
Bun.serve({
  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");
    }

    if (method === "GET" && path === "/api/users") {
      return Response.json([{ id: 1, name: "Alice" }]);
    }

    // Dynamic routes
    const userMatch = path.match(/^\/api\/users\/(\d+)$/);
    if (method === "GET" && userMatch) {
      const userId = userMatch[1];
      return Response.json({ id: userId });
    }

    // 404
    return new Response("Not Found", { status: 404 });
  },
});
```

## Error Handling

```typescript
Bun.serve({
  fetch(req) {
    try {
      // Handle request
      throw new Error("Something went wrong");
    } catch (error) {
      return new Response(
        JSON.stringify({ error: error.message }),
        {
          status: 500,
          headers: { "Content-Type": "application/json" }
        }
      );
    }
  },
  error(error) {
    // Global error handler
    return new Response(`Error: ${error.message}`, { status: 500 });
  },
});
```

## Server Options

```typescript
const server = Bun.serve({
  port: 3000,           // Default: 3000
  hostname: "0.0.0.0",  // Default: "0.0.0.0"

  development: true,    // Pretty errors in browser

  // TLS/HTTPS
  tls: {
    key: Bun.file("./key.pem"),
    cert: Bun.file("./cert.pem"),
  },

  // Unix socket
  unix: "/tmp/my-socket.sock",

  // Max request body size (default 128MB)
  maxRequestBodySize: 1024 * 1024 * 10, // 10MB

  fetch(req) {
    return new Response("OK");
  },
});
```

## Server Methods

```typescript
const server = Bun.serve({ ... });

// Server info
console.log(server.port);        // 3000
console.log(server.hostname);    // "0.0.0.0"
console.log(server.url);         // URL object

// Stop server (Bun 1.2+: returns a Promise — await it to flush in-flight
// requests; calling it without await may not drain connections).
await server.stop();

// Reload with new config
server.reload({
  fetch(req) {
    return new Response("Updated!");
  },
});

// Pending requests count
console.log(server.pendingRequests);
```

## Routing with `routes` (Bun 1.2+)

The examples above use the classic `fetch(req)` handler with manual URL
parsing, which still works. As of Bun 1.2, the canonical way to declare
route maps is the `routes` option, with dynamic params available on
`req.params`:

```typescript
Bun.serve({
  routes: {
    "/api/users": {
      GET: () => Response.json([{ id: 1, name: "Alice" }]),
      POST: async (req) => Response.json(await req.json(), { status: 201 }),
    },
    "/api/users/:id": (req) => {
      const { id } = req.params;
      return Response.json({ id });
    },
  },
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});
```

Use `routes` for new code; keep `fetch` as a fallback for unmatched paths.

## Static Files

```typescript
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);

    // Serve static files from public/
    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");
  },
});
```

## CORS

```typescript
function corsHeaders() {
  return {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
  };
}

Bun.serve({
  fetch(req) {
    // Handle preflight
    if (req.method === "OPTIONS") {
      return new Response(null, { headers: corsHeaders() });
    }

    // Add CORS headers to response
    return new Response("OK", { headers: corsHeaders() });
  },
});
```

## Common Errors

| Error | Cause | Fix |
|-------|-------|-----|
| `EADDRINUSE` | Port in use | Use different port or kill process |
| `Cannot read body` | Body already consumed | Read body once only |
| `CORS error` | Missing headers | Add CORS headers |
| `413 Payload Too Large` | Body exceeds limit | Increase maxRequestBodySize |

## When to Load References

Load `references/tls-config.md` when:
- HTTPS/TLS setup
- Certificate configuration
- mTLS authentication

Load `references/streaming.md` when:
- Server-sent events
- Streaming responses
- Chunked transfer encoding

© secondsky · MIT · secondsky/claude-skills

Similar skills

All skills for Backend and APIs →

From the same set94

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor