Middleware#
Middleware functions intercept requests before they reach your route handler. They can modify the context, short-circuit with a response, or pass control to the next handler.
Middleware Signature#
type Middleware = (
ctx: Context,
next: () => Promise<void | Response>
) => Promise<void | Response>;| Parameter | Description |
|---|---|
| ctx | The request context object |
| next | Call await next() to continue to the next middleware/handler. Omit to short-circuit. |
Basic Middleware#
const logger = async (ctx, next) => {
const start = Date.now();
const response = await next();
const duration = Date.now() - start;
console.log(`${ctx.request.method} ${ctx.request.url} ${duration}ms`);
return response;
};Short-circuit (Block Request)#
const authCheck = async (ctx, next) => {
const token = ctx.request.headers.get("Authorization");
if (!token) {
return ctx.json({ error: "Unauthorized" }, 401);
// Do NOT call next() - request stops here
}
return next(); // Token exists, continue
};Response to short-circuit. Call return next() to continue to the next handler.Global Middleware#
Runs on every request. Register before routes:
const app = new Buntok();
app.use(logger);
app.cors({ origin: "*" }); // Use app.cors() for CORS
app.use(rateLimit);
// All routes inherit these middleware
app.get("/", handler);Route-level Middleware#
Pass middleware directly to route methods. They run in order:
app.get("/admin", authMiddleware, adminMiddleware, handler);
// Execution order: authMiddleware → adminMiddleware → handlerFunctional API#
app.get("/users",
requireAuth(secret),
requireRole("admin"),
async (ctx) => {
return ctx.json({ users: [] });
}
);Decorator API#
@Controller("/users")
class UserController {
@Get("/")
@Use(requireAuth(secret)) // ← evaluated 2nd, applied 3rd → runs 1st
@Use(requireRole("admin")) // ← evaluated 3rd, applied 2nd → runs 2nd
async list(ctx: Context) {
return ctx.json({ users: [] });
}
}
// Evaluate: Controller → requireAuth → requireRole → Get (top-to-bottom)
// Apply: Get → requireRole → requireAuth → Controller (bottom-to-top)unshift). Place @Use(requireAuth) ABOVE @Use(requireRole) so auth runs first.Group Middleware#
Apply middleware to a group of routes with a shared prefix:
const api = app.group("/api");
api.use(cors);
api.use(rateLimit);
api.get("/users", handler); // /api/users
api.get("/posts", handler); // /api/posts
// With requireAuth on group
const admin = app.group("/admin");
admin.use(requireAuth(secret));
admin.get("/dashboard", handler); // /admin/dashboard
admin.get("/settings", handler); // /admin/settingsapp.use) runs first, then Group middleware (group.use), then Route-level middleware (left-to-right in handler args). If any middleware throws an error, it is caught by the framework and passed to the error handler.Middleware with Parameters#
Use a factory function to create middleware with configuration:
// Factory function
function rateLimit(maxRequests: number, windowMs: number) {
const hits = new Map<string, number[]>();
return async (ctx, next) => {
const ip = ctx.ip;
const now = Date.now();
const timestamps = hits.get(ip) || [];
const recent = timestamps.filter(t => t > now - windowMs);
if (recent.length >= maxRequests) {
return ctx.json({ error: "Too many requests" }, 429);
}
recent.push(now);
hits.set(ip, recent);
await next();
};
}
// Usage
app.get("/api", rateLimit(100, 60_000), handler);zValidator Middleware#
Validate request data with Zod schemas:
import { zValidator, z } from "@buntok/core/middlewares/validator";
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.post("/users",
zValidator("body", userSchema),
async (ctx) => {
const data = ctx.valid("body"); // typed
return ctx.json({ created: true });
}
);After-Response Hooks#
Modify the response after the handler completes:
app.get("/data", async (ctx) => {
ctx.onAfterResponse((res) => {
// Add header to every response
const newHeaders = new Headers(res.headers);
newHeaders.set("X-Custom-Header", "value");
return new Response(res.body, {
status: res.status,
headers: newHeaders,
});
});
return ctx.json({ data: "value" });
});Built-in Middleware#
| Middleware | Purpose |
|---|---|
| requireAuth(secret) | JWT authentication |
| requireRole(...) | Role-based access (OR logic) |
| requirePermission(...) | Permission-based access (AND logic) |
| zValidator(target, schema) | Request validation with Zod |
| cors(options) | Cross-Origin Resource Sharing |
| rateLimiter(options) | Fixed-window rate limiting |
| slidingWindowRateLimiter(options) | Sliding-window rate limiting (more accurate) |
| compress(options) | Brotli/gzip response compression |
| bodySizeLimit(options) | Limit request body size |
| requestId(options) | X-Request-Id generation |
| responseTime(options) | X-Response-Time header |
| helmet(options) | Security headers (HSTS, CSP, etc.) |
| timeout(ms, message?) | Request timeout (throws TimeoutError) |
| auditLog(options) | Request logging |
import { requireAuth } from "@buntok/core";
import {
requireRole, requirePermission,
rateLimiter, slidingWindowRateLimiter,
compress, bodySizeLimit, requestId, responseTime, helmet, timeout, auditLog,
} from "@buntok/core";
import { cors } from "@buntok/core";
import { zValidator } from "@buntok/core/middlewares/validator";cors()#
CORS with flexible origin handling (string, array, or function). Handles preflight OPTIONS automatically.
// Recommended - ensures CORS headers on ALL responses including errors
app.cors({
origin: ["http://localhost:3000", "https://myapp.com"],
// origin: (origin) => origin.endsWith(".myapp.com"),
methods: ["GET", "POST", "PUT", "DELETE"],
headers: ["Content-Type", "Authorization"],
credentials: true,
});
// Defaults: methods GET,POST,PUT,DELETE,PATCH,OPTIONS
// headers Content-Type,Authorization,x-api-key
// origin string|string[]|((origin)=>boolean)
origin can be a string, string[], or (origin: string) => boolean for dynamic checks. Use app.cors() instead of app.use(cors(...)) to ensure CORS headers are applied to error responses (4xx, 5xx) as well.requestId()#
Adds X-Request-Id (uuid) to every request. Also available shortId (8-char) and uuid helpers.
import { requestId, shortId, uuid } from "@buntok/core";
app.use(requestId());
// RequestIdOptions { header="x-request-id", generator=uuid, store=true, storeKey="requestId" }
app.use(requestId({ header: "x-correlation-id", generator: shortId }));
app.use(requestId({ header: "x-request-id", generator: () => crypto.randomUUID() }));responseTime()#
Adds X-Response-Time header with request duration.
import { responseTime } from "@buntok/core";
app.use(responseTime());
// ResponseTimeOptions { header="x-response-time", format="ms"|"s", store, storeKey="responseTime" }
app.use(responseTime({ header: "x-response-time", format: "s" }));helmet()#
Security headers (X-Content-Type-Options, X-Frame-Options, XSS-Protection, Referrer-Policy, HSTS, DNS-Prefetch, Permissions-Policy).
import { helmet } from "@buntok/core";
app.use(helmet());
// HelmetOptions { contentTypeOptions, frameOptions, xssProtection, referrerPolicy, hsts, dnsPrefetch, permissionsPolicy, additionalHeaders }
app.use(helmet({
hsts: { maxAge: 31536000, includeSubDomains: true },
frameOptions: "DENY",
}));timeout()#
Aborts handler if it exceeds ms. Throws TimeoutError { timeoutMs } caught by app.onError.
import { timeout, TimeoutError } from "@buntok/core";
app.get("/slow", timeout(5000), async (ctx) => {
await longOperation();
return ctx.json({ ok: true });
});
app.get("/slow2", timeout(5000, "Custom timeout message"), handler);
// Custom error handling
app.onError((err, ctx) => {
if (err instanceof TimeoutError) {
return ctx.json({ error: "Timeout", timeoutMs: err.timeoutMs }, 408);
}
return ctx.json({ error: err.message }, 500);
});compress()#
Response compression using Brotli or gzip, negotiated from Accept-Encoding. Only compresses responses above a size threshold to avoid wasting CPU on small payloads.
// Defaults: threshold=1024, brotliLevel=4
app.use(compress());
// Custom options
app.use(compress({
threshold: 512, // Compress responses > 512 bytes
brotliLevel: 11, // Max compression (slower)
types: ["text/", "application/json", "image/svg+xml"],
}));| Option | Default | Description |
|---|---|---|
| threshold | 1024 | Min response size (bytes) to compress |
| brotliLevel | 4 | Brotli compression level (1-11) |
| types | ["text/", "application/json", ...] | Content-Type prefixes eligible for compression |
bodySizeLimit()#
Limits the maximum request body size. Returns 413 (Payload Too Large) when exceeded. Useful for preventing abuse and protecting server resources.
// Default: 10MB limit
app.use(bodySizeLimit());
// Custom limit (5MB)
app.use(bodySizeLimit({ maxSize: 5 * 1024 * 1024 }));
// Custom error message and status code
app.use(bodySizeLimit({
maxSize: 1024 * 1024, // 1MB
message: "File too large",
statusCode: 413,
}));| Option | Default | Description |
|---|---|---|
| maxSize | 10485760 (10MB) | Max body size in bytes |
| message | "Payload Too Large" | Custom error message |
| statusCode | 413 | HTTP status code for rejection |
Rate Limiters#
Two built-in rate limiters: rateLimiter (fixed window, lower memory) and slidingWindowRateLimiter (more accurate, higher memory).
// Fixed window - 100 requests per minute
app.use(rateLimiter({ max: 100, windowMs: 60000 }));
// Sliding window - more accurate for burst traffic
app.use(slidingWindowRateLimiter({ max: 100, windowMs: 60000 }));
// Custom key & skip & headers
app.use(rateLimiter({
max: 100,
windowMs: 60000,
message: "Too many requests",
statusCode: 429,
headers: true,
skip: (ctx) => ctx.request.headers.get("x-api-key") === "internal",
keyGenerator: (ctx) => ctx.ip, // default ctx.ip
}));| Option | Default | Description |
|---|---|---|
| max | 100 | Max requests per window |
| windowMs | 60000 | Window duration in ms |
| message | "Too many requests..." | Response message |
| statusCode | 429 | HTTP status code |
| headers | true | Add RateLimit-* headers |
| keyGenerator | (ctx)=>ctx.ip | Key for bucket (default ctx.ip) |
| skip | - | Function (ctx)=>boolean to skip |
| store | in-memory Map | Custom RateLimitStore |
Full Example#
import { z } from "@buntok/core/middlewares/validator";
const app = new Buntok();
const secret = process.env.JWT_SECRET!;
// Global middleware
app.cors(); // Use app.cors() for CORS
app.use(rateLimiter({ max: 100, windowMs: 60_000 }));
// Public route
app.get("/", (ctx) => {
return ctx.json({ message: "Public" });
});
// Protected route
app.get("/profile",
requireAuth(secret),
async (ctx) => {
return ctx.json({ user: ctx.user });
}
);
// Admin-only route
app.get("/admin",
requireAuth(secret),
requireRole("admin"),
async (ctx) => {
return ctx.json({ admin: true });
}
);
// Validated route
app.post("/users",
zValidator("body", z.object({
name: z.string().min(1),
email: z.string().email(),
})),
async (ctx) => {
const data = ctx.valid("body");
return ctx.success(data, "Created", 201);
}
);
app.listen(1212);Next Steps#
- Authentication - JWT authentication middleware
- RBAC - Role-based access control
- Validation - Request data validation