Bunintermediate
Bun.serve() with a routes object: a zero-dependency HTTP server
Published July 31, 2026
export const app = Bun.serve({
routes: {
"/api/health": new Response("ok"),
"/api/users/:id": (req) => Response.json({ id: req.params.id }),
"/api/users": {
GET: () => Response.json({ users: ["ada", "grace"] }),
POST: async (req) => Response.json({ created: true, ...(await req.json()) }, { status: 201 }),
},
"/api/*": Response.json({ error: "Not found" }, { status: 404 }),
},
fetch: () => new Response("Not found", { status: 404 }),
});
console.log(`Listening on ${app.url}`);What
Bun.serve()'s routes option (Bun 1.2.3+) matches an incoming request against a map of
paths — static, :param, per-HTTP-method, and wildcard — before your fetch handler ever
runs, so a small REST API needs no router package.
Why it matters
Before routes existed, every Bun.serve() app hand-rolled its own routing inside a single
fetch(req) function: parse the URL, switch on req.method, string-match or regex the
pathname, pull params out manually. That's exactly the job express, hono, and similar
packages exist to do — and it's also exactly what a route table expresses more directly. Bun
now does that matching natively, in the same native code path that makes Bun.serve() fast,
with no added dependency, install size, or startup cost.
How it works
"/api/health"maps a path directly to aResponsevalue — Bun reuses the sameResponseobject on every request, since it's immutable and this is a static route."/api/users/:id"is a dynamic route: the matched segment is available asreq.params.idinside the handler, with no manual parsing."/api/users"maps to an object of HTTP methods instead of a single function, soGETandPOSTon the same path are two separate handlers rather than oneif (req.method === ...)."/api/*"is a wildcard fallback for anything under/api/that didn't match a more specific entry above it — Bun prefers the most specific match, so this only catches the leftovers.- The top-level
fetchhandler is the fallback for everythingroutesdoesn't match at all.
Gotchas
- Route matching supports exact paths and
:paramsegments, not arbitrary regex — a wildcard prefix like/api/*is the closest thing to a catch-all, not a full regex route. routesrequires Bun 1.2.3 or newer. On older versions it's silently ignored, sofetchneeds to handle every path itself.server.reload({ routes })swaps the route table on a running server without downtime — useful for tests or hot-reloading route definitions without restarting the process.
Related
- Serving a file lazily as a route value (
"/favicon.ico": Bun.file("./favicon.ico")) — see the Bun.file streaming post for the general pattern. - WebSocket upgrades still go through
fetch/server.upgrade(), notroutes— routes are HTTP-only.