Router Deep Dive
bunway’s router borrows Express’ ergonomics while staying true to Bun’s Fetch APIs. This page explains the lifecycle so you can compose middleware, sub-routers, and custom responses with confidence.
Anatomy of a request
- Match – incoming requests match routes by HTTP method + path (supporting
:params). - Pipeline – global middleware → auto body parser → route-specific middleware/handlers.
- Execution – each handler receives
(ctx, next). Callawait next()to continue the chain. - Finalization – the router chooses the final
Response(explicit return,ctx.res.last, or default 200) and merges header bags (e.g., CORS) before returning.
Registering routes
Define routes using the familiar HTTP verb helpers. Each handler receives the Bun-native context object, giving you immediate access to parameters, query strings, locals, and body parsing utilities.
const app = bunway();
app.get("/health", (ctx) => ctx.res.text("OK"));
app.post("/users", async (ctx) => ctx.res.created(await ctx.req.parseBody()));
app.patch("/users/:id", async (ctx) => {
const id = ctx.req.param("id");
const updates = await ctx.req.parseBody();
return ctx.res.json(await updateUser(id, updates));
});Multiple handlers
Chain middleware the same way you would in Express. Each handler can perform work, populate ctx.req.locals, and decide whether to continue by calling await next().
app.get("/users/:id", authMiddleware, loadUser, (ctx) => ctx.res.json(ctx.req.locals.user));next() is promise-based—await it if you need to run code after downstream handlers complete.
Middleware ordering
Global middleware runs in the order registered, followed by route-specific middleware. This makes it easy to compose logging, authentication, and other cross-cutting concerns.
app.use(cors()); // global
app.use(json()); // global
app.use(loggingMiddleware); // global
app.get("/secure", authMiddleware, (ctx) => ctx.res.ok(ctx.req.locals.user));- Global middleware runs before route-specific middleware.
ctx.req.isBodyParsed()lets you skip redundant parsing.- Middleware can return
Responseto short-circuit the pipeline (e.g., auth failures).
Sub-routers
Group related endpoints into sub-routers for better organization. bunway rewrites the request URL so nested routers see paths relative to their mount point.
import { Router } from "bunway";
const api = new Router();
api.get("/users", listUsers);
api.get("/users/:id", showUser);
app.use("/api", api);Sub-routers inherit parent middleware and can register their own router.use() handlers.
Sub-router inheritance
Middleware registered on the parent app runs before sub-router handlers. Add router-specific middleware (auth, logging) inside the router for scoped behaviour.
Nested routers
Routers can be nested multiple levels deep. This lets large applications expose modular areas (e.g., /api/admin) without losing composability.
const admin = new Router();
admin.use(requireAdmin);
admin.get("/stats", getStats);
api.use("/admin", admin);Returning native responses
Handlers can always return Response objects straight from Fetch APIs—bunWay will still merge any middleware headers during finalization.
Error handling
- Throw
HttpErrorfor explicit status/body/headers. - Throw/return
Responsefor fully custom responses. - Use
errorHandler()middleware for logging/mapping. - Unhandled errors fall back to a safe 500 JSON payload.
app.get("/secret", () => {
throw new HttpError(403, "Forbidden");
});404 behaviour
Unmatched routes return:
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error":"Not Found"}Customize by adding a catch-all route at the end:
app.use((ctx) => ctx.res.status(404).json({ error: "Route not found" }));Catch-all
Be sure to register catch-all handlers last—bunway processes middleware in order, so earlier routes or middleware can short-circuit the response.
Body parser defaults
Routers accept body parser defaults via constructor:
const router = new Router({
bodyParser: {
json: { limit: 2 * 1024 * 1024 },
text: { enabled: true },
},
});Handlers can override parsing dynamically with ctx.req.applyBodyParserOverrides().
Recipes — run everything, the Bun way
Friendly request logger
app.use(async (ctx, next) => {
const start = performance.now();
await next();
const ms = (performance.now() - start).toFixed(1);
console.log(`${ctx.req.method} ${ctx.req.path} → ${ctx.res.statusCode} (${ms}ms)`);
});Flip logging on or off with environment variables (e.g., BUNWAY_LOG_REQUESTS=true) to keep production output tidy.
Admin-only sub-router
const admin = new Router();
// bring HttpError in from "bunway" to reuse friendly responses
admin.use(async (ctx, next) => {
if (ctx.req.headers.get("authorization") !== "super-secret") {
throw new HttpError(401, "Admin authorization required");
}
await next();
});
admin.get("/stats", (ctx) => ctx.res.json({ uptime: process.uptime() }));
app.use("/admin", admin);Per-request format switch
app.post("/webhook", async (ctx) => {
ctx.req.applyBodyParserOverrides({ text: { enabled: true }, json: { enabled: false } });
const payload = await ctx.req.parseBody();
return ctx.res.ok({ received: payload });
});::: note Configuration tip Combine these recipes with app.use(bunway.bodyParser({ text: { enabled: true } })) to set defaults before per-request overrides kick in. :::
Advanced patterns
- Streaming: work directly with
await ctx.req.rawBody()orctx.req.original.bodyfor streams. - Locals: share data across middleware via
ctx.req.locals(e.g.,ctx.req.locals.user = user). - Async cleanup: run code after
await next()to implement logging, timers, or metrics.
Continue to Middleware Overview or explore type-level details in the API Reference.