@nds-stack/bun-middleware
v0.1.0-alpha.0
Published
Middleware pipeline and router for Bun.serve — chain middleware, route requests, zero dependencies
Downloads
85
Maintainers
Readme
@nds-stack/bun-middleware
Middleware pipeline and router for Bun.serve — chain middleware, route requests, zero dependencies.
Why bun-middleware
Bun.serve has two modes: routes (fast but no middleware) and fetch (manual routing with middleware). bun-middleware gives you both — a clean middleware pipeline + declarative routing.
import { createApp, getParams } from "@nds-stack/bun-middleware";
const app = createApp();
app.use(async (req, next) => {
console.log(`${req.method} ${req.url}`);
return next();
});
app.get("/users/:id", (req) => {
const { id } = getParams(req);
return Response.json({ userId: id });
});
Bun.serve(app.serve());How It Works
Request -> Global Middleware Chain -> Router.match()
├── GET /users/:id -> handler (with params)
├── POST /data -> handler
└── 404 Not FoundEach middleware calls next() to pass control down the chain. A middleware can short-circuit by returning a Response directly. Route params are accessed via getParams(req) using a Symbol — no request mutation.
Installation
bun add @nds-stack/bun-middlewareAPI
createApp()
Returns an AppInstance with chainable methods:
| Method | Description |
|--------|-------------|
| use(...middleware) | Add middleware to global chain |
| get(path, ...handlers) | Register GET route |
| post(path, ...handlers) | Register POST route |
| put(path, ...handlers) | Register PUT route |
| delete(path, ...handlers) | Register DELETE route |
| patch(path, ...handlers) | Register PATCH route |
| fetch(req) | Execute middleware chain + routing for a Request |
| serve(options?) | Start Bun.serve and return server info |
Middleware
type Middleware = (req: Request, next: NextFunction) => Response | Promise<Response>;Middleware receives the Request and a next() function. Call next() to pass to the next middleware or route handler. Return a Response directly to short-circuit.
Route Patterns
| Pattern | Example | Matches |
|---------|---------|---------|
| Static | /users | /users |
| Param | /users/:id | /users/42 |
| Multiple | /posts/:year/:slug | /posts/2024/hello |
getParams(req)
Returns route parameters as an object:
app.get("/users/:id", (req) => {
const { id } = getParams(req); // { id: "42" }
return Response.json({ id });
});serve(options?)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| port | number | 3000 | Server port |
| hostname | string | 0.0.0.0 | Server hostname |
| tls | { key, cert } | — | TLS config |
| error | (req, err) => Response | 500 handler | Custom error handler |
| notFound | (req) => Response | 404 handler | Custom 404 handler |
Returns { port, hostname, stop }.
Error Handling
const app = createApp();
app.get("/data", () => {
throw new Error("db failure");
});
const server = app.serve({
error: (req, err) => {
console.error(err);
return new Response("Custom error", { status: 500 });
},
});Limitations
- Wildcard (
*) route support included - No static file serving (planned)
- No built-in body parsing or CORS middleware (separate modules)
- Route params via Symbol (not
req.params) — intentional to avoid mutation - Unhandled errors in middleware/routes are caught by
serve()and logged to console. Provide a customerrorhandler to suppress console output.
Multi-Instance / Cross-Boundary
Each createApp() returns an independent instance. For multiple servers:
const api = createApp();
const admin = createApp();
api.get("/users", () => Response.json([...]));
admin.get("/config", () => Response.json({}));
const s1 = api.serve({ port: 3001 });
const s2 = admin.serve({ port: 3002 });Customization Guide
Custom Error Handler
const app = createApp();
const server = app.serve({
error: (req, err) => {
return Response.json({ error: err.message }, { status: 500 });
},
});Multiple Handlers per Route
app.get("/profile",
async (req, next) => { /* validate auth */; return next(); },
async (req, next) => { /* load user */; return next(); },
(req) => { return Response.json({ profile: true }); },
);Comparison Table
| Feature | @nds-stack/bun-middleware | Hono | Elysia |
|---------|:---------------------------:|:----:|:------:|
| Dependencies | 0 | 0 | 0 |
| Bundle size | ~4KB | ~14KB | ~60KB |
| Middleware chain | ✅ | ✅ | ✅ |
| Route params | ✅ | ✅ | ✅ |
| Sub-app mounting | ⏳ Future | ✅ | ✅ |
| WebSocket | ⏳ Future | ✅ | ✅ |
| Built-in CORS | — | ✅ | ✅ |
| Type inference | Basic | Full | Full |
Benchmarks
Note on overhead: bun-middleware adds ~50-70% overhead compared to a raw handler function because each request traverses a closure-based middleware chain (recursive
next()calls) and a linear-scan route matcher. You gain zero-dependency composable middleware and declarative routing. For most applications, this overhead is negligible (microseconds) compared to I/O-bound operations like database queries or external API calls.
| Operation | @nds-stack/bun-middleware | Raw handler | Overhead |
|-----------|:--------------------------:|:-----------:|:--------:|
| Route match | ~289K ops/s | ~642K ops/s | ~55% |
| 1 middleware | ~323K ops/s | ~642K ops/s | ~50% |
| 5 middleware | ~188K ops/s | ~642K ops/s | ~71% |
Benchmark: 5000 iterations, Bun v1.3.14. Run bun run bench for current results.
Run your own: bun run bench
Real-World Example
import { createApp, getParams } from "@nds-stack/bun-middleware";
const app = createApp();
app.use(async (req, next) => {
const start = performance.now();
const res = await next();
const ms = (performance.now() - start).toFixed(2);
console.log(`${req.method} ${req.url} ${res.status} ${ms}ms`);
return res;
});
app.get("/api/users", async () => {
const users = await (await fetch("https://jsonplaceholder.typicode.com/users")).json();
return Response.json(users);
});
app.get("/api/users/:id", async (req) => {
const { id } = getParams(req);
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) return new Response("Not found", { status: 404 });
const user = await res.json();
return Response.json(user);
});
app.post("/api/users", async (req) => {
const body = await req.json();
return Response.json({ created: body, id: Date.now() }, { status: 201 });
});
app.serve({ port: 3000 });