Bun.serve routes — adopt native routes map (Hono removed)
Accept — and promote to the primary routing mechanism. Originally rejected because Hono's middleware model would diverge from Bun.serve({ routes }). With the…
Review verdict (2026-05-24)
Accept — and promote to the primary routing mechanism. Originally rejected because Hono's middleware model would diverge from Bun.serve({ routes }). With the Hono-removal decision (2026-05-24), that objection disappears: there's only one router, and it's Bun.serve's native routes: map on the Bun target. The edge adapter (12) compiles the same route table into a plain fetch dispatcher for vendors whose runtime doesn't accept routes: natively.
This RFC now subsumes spec 02's old "Hono assembly" purpose:
- 02b's
RouteEntry[]is consumed by 01 to build theroutes:object directly. - Middleware composes via the new
ctx-based linear composer (decided 2026-05-24) — wrapped around each handler at registration time, not by a Honoapp.use. - Per-route handlers receive
(req, ctx)where Bun's nativereq.paramspopulatectx.params.
The "two code paths kept in sync" concern from the original draft is real but bounded: the edge adapter implements a tiny JS matcher (~30 lines) that walks the same RouteEntry[]. Conformance is enforced by running the integration fixture set against both targets.
Summary
Modern Bun.serve accepts a routes: object alongside fetch: — keys are path patterns (/users/:id), values are either method-keyed handler objects or Response instances. Bun matches the route in C++ before calling JS, with typed req.params. Patties should mount its compiled route table through routes: instead of routing entirely in JS inside fetch:.
Motivation
01-server today wires fetch to call router.fetch which iterates the compiled table. That's correct and fast, but loses Bun's native route matcher, native param parsing, and the static-html-import shortcut. Pages and static: already use the native fast path; API routes should too.
Proposal
- 01-server: build a
routes:object from theRouteEntry[]. Each handler is wrapped in the framework's middleware composer before registration so user middleware sees every request. Thefetch:handler is the catch-all 404. - 02-router: this spec's purpose becomes "compose the routes object" — it no longer assembles a Hono app. Pattern compilation emits Bun-compatible strings (
/users/:id,/files/*). - 02b: filesystem router's output already provides paths; add a
bunPatternfield consumed by 01. - 07-middleware: the framework owns a ~50-line linear composer; handlers receive
(req, ctx). - 09-plugins:
setup(server, ctx)receives the route-registration API (server.route(pattern, methodHandlers)) instead of a Honoapp. - 12-edge-adapters: edge target compiles the same
RouteEntry[]into afetch(req)dispatcher (~30 lines of matcher). Same handlers, samectx, different entrypoint shape.
Sample:
Bun.serve({
routes: {
"/api/users/:id": {
GET: compose([userMiddleware], handlers.usersGet),
POST: compose([userMiddleware], handlers.usersPost),
},
"/healthz": new Response("ok"),
},
fetch: compose([userMiddleware], notFoundHandler),
});
Trade-offs
- Two route-dispatch implementations (Bun-native
routes:vs. edge JS matcher). Bounded by a sharedRouteEntry[]and conformance tests. - Removing Hono means losing the Hono middleware ecosystem (
hono/jwt,hono/cors, etc.). Trade-off accepted — equivalents land as@patties/*plugins.
Open questions
- Param typing — generate
.d.tsfrom the route table at build time, or rely on Bun's inferredreq.params? Leaning generated, for editor go-to-def. - How does the middleware composer surface in user code? Likely
defineMiddleware(handler)frompatties/middleware.