Router
Compile the user's app/routes/ directory into the routes: object consumed by 01-server. This spec is the integration point between the filesystem layout and …
Purpose
Compile the user's app/routes/ directory into the routes: object consumed by 01-server. This spec is the integration point between the filesystem layout and Bun.serve's native dispatcher. There is no HTTP framework — the output is a plain Record<pattern, methodHandlers> plus a fallback handler.
Public surface
export interface CompiledRouter {
routes: BunRoutes // shape consumed by Bun.serve({ routes })
fallback: (req: Request) => Promise<Response>
}
export async function createRouter(options: RouterOptions): Promise<CompiledRouter>
RouterOptions:
appDir: string— absolute path to user'sapp/folder.middleware?: Middleware— global middleware fromapp/middleware.ts(see 07-middleware).plugins?: Plugin[]— resolved plugins from 09-plugins.renderer: Renderer— from 03-render.
Behavior
Wiring order is fixed: user middleware → plugins → routes.
- Read the build-time route table (in production, inlined via a Bun macro per 04-build; in dev, computed at boot via 02b).
- Compose the middleware chain:
[userMiddleware, ...pluginMiddlewares].reduce(...)produces awrap(handler)function that returns a middleware-wrapped handler. This is the framework-owned composer (~50 lines) from 07. - Iterate plugin
setup(server, ctx)calls in declared order (09). Plugins register routes viaserver.route(pattern, methodHandlers); the framework adds those routes to the sameroutesmap after user middleware wrapping. Plugin-registered routes are wrapped by user middleware just like filesystem routes. - For each
RouteEntry:- Page routes (
app/routes/**/*.tsx): produce aGEThandler that callsrenderer.renderPage(entry, req, ctx). - API routes (
app/routes/api/**/*.ts): import the module; for each exported HTTP method (GET,POST,PUT,DELETE,PATCH,OPTIONS), wrap with the middleware composer and add toroutes[pattern][method].
- Page routes (
- Return
{ routes, fallback }.fallbackis the middleware-wrapped 404 handler.
Conventions enforced
- Dynamic segments:
[param].tsx→:param,[...rest].tsx→*(Bun's catch-all). - Index files:
index.tsx→ trailing-slash-stripped parent path. app/routes/api/*always becomes/api/*.- Page routes only respond to
GET; mutating verbs belong in API routes. - Pattern strings emitted match
Bun.serve({ routes })syntax exactly (no custom matcher dialect).
Method-not-allowed
Patterns that match the URL but lack the requested method return 405 automatically — Bun.serve({ routes }) does this for us when a method-keyed object is provided and the method is missing.
Non-goals
- A handwritten path matcher on the Bun target.
Bun.serve({ routes })is the matcher. - Route-level config files (
page.config.ts) — defer to a future RFC. - Per-route middleware files — composition happens inside
app/middleware.tsfor now; a filesystem syntax is a future RFC.
Acceptance criteria
- Given
app/routes/index.tsx,GET /invokes the renderer with that module's default export. - Given
app/routes/hotels/[city].tsx,GET /hotels/baliresolves withreq.params.city === "bali"(populated by Bun's native matcher). - Given
app/routes/api/revenue.tsexportingGET,GET /api/revenueinvokes that export through the middleware composer. - Unknown methods on a known API route return
405(handled byBun.serve, not by framework code). - The compiled
routesobject contains no closures over Hono types or any HTTP framework — it's a plain map of plain functions.