A small TypeScript router for Bun with chainable route registration, reusable middleware chains, router-wide middleware defaults for future routes, and optional route auto-loading from a directory.
bun add @yiffy/bun-routerThe package exports:
default: a shared router instanceRouter: the router class if you want your own instanceRouteChain: the chain object returned byRouter.new()andMiddlewareChain.new()MiddlewareChain: the chain object returned byrouter.use(...)Middleware: built-in middleware helpersAnyBunServer,Middleware, andHandler: TypeScript types for server, middleware, and handlers
import router from "@yiffy/bun-router";
router
.new("/hello", "GET")
.handle(() => Response.json({ message: "hello" }));
Bun.serve({
port: 3000,
routes: router.toRoutes(),
});Create a route with router.new(path, method?).
- When
methodis provided, the route is registered for that HTTP method only. - When
methodis omitted, the handler is registered directly on the path. - Duplicate path or method/path registrations throw an error.
import { Router } from "@yiffy/bun-router";
const router = new Router();
router
.new("/users/:id", "GET")
.handle((req) => {
return Response.json({
id: req.params.id,
});
});
router
.new("/health")
.handle(() => new Response("ok"));Middleware receives (req, server, next) and must return a Response or a promise for one.
router.use(...)starts a reusable middleware chainrouter.useAll(...)adds middleware to all future routes created from that routerrouter.unuseAll(...)removes a router-wide middleware for future routesroute.use(...)adds middleware for a single routemiddlewareChain.use(...)adds middleware to a reusable chainmiddlewareChain.unuse(...)removes middleware from that reusable chainmiddlewareChain.new(...)creates a route preloaded with that chain's middlewarenext(req, server)continues the chain
import { Router, Middleware, type AnyBunServer } from "@yiffy/bun-router";
const router = new Router();
const auth = (req: Bun.BunRequest<"/private">, server: AnyBunServer, next: (req: Bun.BunRequest<"/private">, server: AnyBunServer) => Response | Promise<Response>) => {
if (req.headers.get("authorization") !== "some secret") {
return Response.json({ message: "Unauthorized" }, { status: 401 });
}
return next(req, server);
};
const requestId = Middleware.Before((req) => {
req.headers.set("x-request-id", crypto.randomUUID());
});
router.useAll(Middleware.DebugLog);
router.useAll(requestId);
router
.new("/private", "GET")
.use(auth)
.handle(() => Response.json({ ok: true }));
const api = router
.use(Middleware.DebugLog("api:"));
api
.new("/api/ping", "GET")
.handle(() => Response.json({ ok: true }));router.load(directory) imports every *.ts file in a directory.
This is useful when route modules register themselves against the default router:
// routes/hello.ts
import router from "@yiffy/bun-router";
router
.new("/hello", "GET")
.handle(() => new Response("hello"));// server.ts
import router from "@yiffy/bun-router";
await router.load("./routes");
Bun.serve({
routes: router.toRoutes(),
});Notes:
- only
*.tsfiles are scanned - importing a route file should have the side effect of registering its routes
- call
router.toRoutes()to retrieve the Bun routes object after loading
If a handler or middleware throws or rejects, the router catches the error and returns a 500 JSON response:
{ "message": "ErrorName: message" }If the thrown error has an Error cause, the cause is appended to the message.
If you want custom error responses inside the middleware chain, add Middleware.Recover(...) near the start of the chain.
Creates an isolated router instance.
Starts a RouteChain.
Starts a MiddlewareChain that can be extended and used to create routes sharing the same middleware stack.
Adds middleware to all future routes created from that router. It does not apply retroactively to routes that already exist.
Removes a previously-added router-wide middleware from future routes. The function reference must match exactly.
Adds middleware to that middleware chain.
Removes middleware from that middleware chain. The function reference must match exactly.
Starts a RouteChain preloaded with the middleware stored on that chain.
Adds middleware to the current route chain.
Finalizes the route and registers it on the router.
Loads route files from a directory.
Returns the route table to pass into Bun.serve({ routes }).
Wraps a request-side effect function and turns it into router middleware. The injected handler runs before next(req, server).
Runs a side effect after downstream middleware and the handler complete. The handler receives both the request and the final response, and the original response object is returned unchanged.
Alias for Middleware.Before(handler).
Logs requests through persistent-debug defaulting to the namespace prefix server:. <method> is appended to the prefix for each request. For example, a GET /hello request would log with the namespace server:get.
It can be passed directly as middleware or called with a namespace prefix:
Middleware.DebugLogMiddleware.DebugLog("api:")
persistent-debug is an optional dependency. If it is not installed, the middleware emits a one-time process warning instead of throwing.
Adds or forwards a request ID header. By default it uses x-request-id, writes the value onto the request before the handler runs, and mirrors it onto the response.
If provided, options.generator(req, server) is used to create the ID.
It can be passed directly as middleware or configured with options:
Middleware.RequestIdMiddleware.RequestId({ headerName: "x-correlation-id" })
Measures request duration and appends a Server-Timing entry to the response. It can also call an optional logger hook as log(durationMs, req, response, server).
It can be passed directly as middleware or configured with options.
Adds standard CORS headers and handles preflight OPTIONS requests. It supports fixed origins, allowlists, or a callback for dynamic origin resolution via origin(originHeader, req, server).
It can be passed directly as middleware or configured with options.
Rejects requests whose body exceeds a configured byte limit with a 413 JSON response. It checks content-length when available and otherwise measures the streamed body through a cloned request.
Intercepts downstream errors inside the middleware chain and maps them to a custom response. This is useful when you want route-specific or API-specific error formatting instead of the router's default 500 response.
options.handler(error, req, server) can return a custom response, and options.log(error, req, server) can be used for custom error logging.
It can be passed directly as middleware or configured with options.
Replaces the request object before downstream middleware and the handler run. The handler receives the current request and must return the request instance that should continue through the chain.
Parses the Authorization: Basic ... header, calls a validation callback with the decoded username and password, and returns a 401 response with a WWW-Authenticate challenge when authentication fails.
Parses the Authorization: Bearer ... header, calls a validation callback with the token, and returns a 401 bearer challenge when authentication fails.
Overrides the request method using x-http-method-override or a _method query parameter before calling the next middleware or handler.
This does not change Bun's initial route dispatch. It is most useful on method-agnostic routes or when your handler inspects req.method directly.
It can be passed directly as middleware or configured with options.
Computes an ETag for successful GET and HEAD responses, sets the header, and returns 304 Not Modified when the request If-None-Match value matches.
If provided, options.generator(body, response, req, server) is used before the default SHA-1 based tag generation.
It can be passed directly as middleware or configured with options.
Replaces the downstream response before it is returned. The handler receives the response produced by later middleware or the route handler and must return the response that should continue back up the chain.
Applies a fixed-window rate limit with an in-memory store by default. It can emit standard limit headers and accepts custom key, skip, and storage functions.
import { Middleware } from "@yiffy/bun-router";
const cors = Middleware.CORS({
allowCredentials: true,
allowHeaders: ["content-type", "authorization", "x-request-id"],
allowMethods: ["GET", "POST", "OPTIONS"],
origin: ["https://app.example.com", "https://admin.example.com"],
});
const before = Middleware.Before((req) => {
req.headers.set("x-request-id", crypto.randomUUID());
});
const after = Middleware.After((_req, res) => {
res.headers.set("x-finished", "1");
});
const requestId = Middleware.RequestId;
const timing = Middleware.Timing;
const configuredTiming = Middleware.Timing({ metricName: "router" });
const bodyLimit = Middleware.BodyLimit({ maxBytes: 1024 * 1024 });
const debugLog = Middleware.DebugLog;
const apiDebugLog = Middleware.DebugLog("api:");
const inject = Middleware.Inject((req) => {
req.headers.set("x-legacy-hook", "1");
});
const recover = Middleware.Recover;
const modifyRequest = Middleware.ModifyRequest((req) => new Request(req, {
headers: {
...Object.fromEntries(req.headers),
"x-replaced-request": "1",
},
}) as Bun.BunRequest);
const auth = Middleware.BearerAuth({
validate: (token) => token === process.env.API_TOKEN,
});
const etag = Middleware.ETag;
const modifyResponse = Middleware.ModifyResponse((res) => new Response(res.body, {
headers: {
...Object.fromEntries(res.headers),
"x-replaced-response": "1",
},
status: res.status,
statusText: res.statusText,
}));
const rateLimit = Middleware.RateLimit({ limit: 60, windowMs: 60_000 });
const methodOverride = Middleware.MethodOverride;bun install --frozen-lockfile --ignore-scripts
bun run lint
bun run buildThe build outputs runtime code and declaration files into dist/.