bun.directory

Search bun.directory

Find a skill or the Node package you want to replace

GitHubSubmit

Bun.serve routes

Queued

Use when building an HTTP server, REST API, webhook receiver or static file server in a Bun project. Use Bun.serve with the routes option instead of express, fastify, koa or node:http.

bunx bun.directory add bun-serve-routes
View source
Verification lift

Queued. The verifier will run each of the 2 tasks with and without this skill, then score the lift.

2 tasks

SKILL.md

4 sections

Bun has a fast HTTP server built in. Bun.serve takes a routes object with path patterns, per-method handlers and static responses. You do not need express, fastify, koa or node:http, and you should not install them.

The shape

const server = Bun.serve({
  port: Number(process.env.PORT ?? 3000),
  routes: {
    // Static response: built once, served with zero allocation per request.
    "/health": new Response("ok"),

    // One handler for every method.
    "/api/version": () => Response.json({ version: "1.0.0" }),

    // Per-method handlers. Params are typed from the path: req.params.id is a string.
    "/api/todos/:id": {
      GET: (req) => Response.json(getTodo(req.params.id)),
      DELETE: (req) => {
        deleteTodo(req.params.id);
        return new Response(null, { status: 204 });
      },
    },
    "/api/todos": {
      GET: () => Response.json(listTodos()),
      POST: async (req) => {
        const body = await req.json();
        return Response.json(createTodo(body), { status: 201 });
      },
    },

    // Wildcards match the rest of the path.
    "/api/*": Response.json({ error: "Not found" }, { status: 404 }),
  },

  // Runs when no route matches. Return a JSON 404 for APIs.
  fetch() {
    return Response.json({ error: "Not found" }, { status: 404 });
  },

  // Uncaught errors in handlers end up here.
  error(err) {
    console.error(err);
    return Response.json({ error: "Internal Server Error" }, { status: 500 });
  },
});

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

Rules

  • Read the port from process.env.PORT with a default. Use port: 0 in tests to get a free port, then read server.port or server.url.
  • Handlers return a Response (or a promise of one). Use Response.json(data, init) for JSON. Don't build JSON strings by hand.
  • Route precedence is by specificity, not order: exact paths beat :params, which beat * wildcards. Unmatched methods on a method-object route fall through to fetch.
  • Validate the body yourself. await req.json() throws on invalid JSON; catch it and return a 400.
  • Query strings: new URL(req.url).searchParams.
  • Headers: req.headers.get("authorization"). Set response headers through new Response(body, { headers }).
  • Cookies: req.cookies.get("session") and req.cookies.set(...), which applies Set-Cookie automatically.
  • Static files: new Response(Bun.file("./public/logo.png")) streams the file with the right Content-Type. Import an HTML file (import home from "./index.html") and pass it as a route value to get bundled frontend assets.
  • Static directories (Bun 1.4+): "/static/*": { dir: "./public" } serves a folder with sendfile, ETag/304, Range and traversal protection. Don't hand-roll a /static/* handler with a .. check. The route opens the directory when Bun.serve starts and keeps it open, so empty a build output folder between rebuilds instead of deleting and recreating it, or the route serves 404s from the deleted folder.
  • Compiled binaries: files embedded with bun build --compile --asset <dir> live under /$bunfs/root/<dir name>/. { dir } routes and bare Bun.file() route values can't read /$bunfs/; serve each file as new Response(Bun.file(path)) (static) or from a handler.
  • Graceful shutdown: await server.stop() lets in-flight requests finish.
  • Testing: start the server with port: 0 inside bun:test, call it with fetch(new URL("/path", server.url)), and stop it in afterAll.

Don't

  • Don't install express, fastify, koa, body-parser, cors, or node-fetch.
  • Don't import from node:http or http. Bun.serve is the server.
  • Don't call app.listen()-style APIs. Bun.serve starts listening as soon as it is called.
  • Don't wrap static responses in a function; pass the Response directly so Bun can cache it.

CORS without a package

const CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "content-type" };

routes: {
  "/api/items": {
    OPTIONS: () => new Response(null, { status: 204, headers: CORS }),
    GET: () => Response.json(items, { headers: CORS }),
  },
}

If the project needs middleware chains, validation helpers or a typed RPC client, use Hono on Bun (see the hono-on-bun skill), not express.

How the verifier tests it

01
health-and-staticTurn this project into a small web server. - GET /health returns the text "ok". - GET / serves the file public/index.html with an HTML content type. - GET /api/echo?msg=hello returns {"msg": "hello"} as JSON. - POST /api/echo returns the JSON body it was sent. Invalid JSON returns status 400. - Unknown paths return status 404 with a JSON body {"error": "Not found"}. Create public/index.html with any content containing the word "bun". The server must listen on the PORT environment variable and start with `bun run start`.
02
todos-apiBuild a JSON REST API for todos in this project. Keep todos in memory. - GET /todos returns all todos as an array. - POST /todos with {"title": string} creates a todo {id, title, done: false} and returns it with status 201. Return 400 if title is missing. - GET /todos/:id returns one todo, or 404 if it doesn't exist. - PATCH /todos/:id with {"done": boolean} updates it and returns it. - DELETE /todos/:id deletes it and returns 204. - Unknown paths return a JSON body {"error": "Not found"} with status 404. The server must listen on the port in the PORT environment variable and start with `bun run start`.
Requires
Bun >=1.2.3
Tasks
2
Author
bgribbin

Other ways to install

bunx bun.directory add bun-serve-routes