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#

typescript
type Middleware = (
  ctx: Context,
  next: () => Promise<void | Response>
) => Promise<void | Response>;
ParameterDescription
ctxThe request context object
nextCall await next() to continue to the next middleware/handler. Omit to short-circuit.

Basic Middleware#

typescript
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)#

typescript
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
};
Warning
Return a Response to short-circuit. Call return next() to continue to the next handler.

Global Middleware#

Runs on every request. Register before routes:

typescript
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:

typescript
app.get("/admin", authMiddleware, adminMiddleware, handler);

// Execution order: authMiddleware → adminMiddleware → handler

Functional API#

typescript
app.get("/users",
  requireAuth(secret),
  requireRole("admin"),
  async (ctx) => {
    return ctx.json({ users: [] });
  }
);

Decorator API#

typescript
@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)
Info
Stage 3 ordering: factories evaluated top-to-bottom but applied bottom-to-top (via unshift). Place @Use(requireAuth) ABOVE @Use(requireRole) so auth runs first.

Group Middleware#

Apply middleware to a group of routes with a shared prefix:

typescript
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/settings
Info
Execution order: Global middleware (app.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:

typescript
// 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:

typescript
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:

typescript
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#

MiddlewarePurpose
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
typescript
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.

typescript
// 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)
Info
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.

typescript
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.

typescript
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).

typescript
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.

typescript
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.

typescript
// 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"],
}));
OptionDefaultDescription
threshold1024Min response size (bytes) to compress
brotliLevel4Brotli 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.

typescript
// 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,
}));
OptionDefaultDescription
maxSize10485760 (10MB)Max body size in bytes
message"Payload Too Large"Custom error message
statusCode413HTTP status code for rejection

Rate Limiters#

Two built-in rate limiters: rateLimiter (fixed window, lower memory) and slidingWindowRateLimiter (more accurate, higher memory).

typescript
// 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
}));
OptionDefaultDescription
max100Max requests per window
windowMs60000Window duration in ms
message"Too many requests..."Response message
statusCode429HTTP status code
headerstrueAdd RateLimit-* headers
keyGenerator(ctx)=>ctx.ipKey for bucket (default ctx.ip)
skip-Function (ctx)=>boolean to skip
storein-memory MapCustom RateLimitStore

Full Example#

typescript
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#