Middleware

Middleware in Katal can intercept requests before they reach a controller and modify responses afterwards. Every middleware implements the Middleware interface:

interface Middleware {
  before?(ctx: MiddlewareContext): Promise<Response | null>;
  after?(ctx: MiddlewareContext, response: Response): Promise<Response>;
}
  • before — Return a Response to short-circuit the pipeline (e.g. 401), or null to continue.
  • after — Mutate or replace the response before it is sent (e.g. inject headers).

Global Middleware

Applied to every request:

import { Application } from "katal";

const app = new Application({ port: 3000 });

app.use({
  async before(ctx) {
    console.log(`[${new Date().toISOString()}] ${ctx.request.method} ${new URL(ctx.request.url).pathname}`);
    return null; // allow request to continue
  },
});

Named Middleware

Register a middleware under a string key and reference it on individual routes or route groups:

app.middleware("require-api-key", {
  async before(ctx) {
    const key = ctx.request.headers.get("X-Api-Key");
    if (key !== process.env.API_KEY) {
      return new Response(JSON.stringify({ message: "Forbidden" }), {
        status: 403,
        headers: { "Content-Type": "application/json" },
      });
    }
    return null;
  },
});

// Use on a route
router.get("/webhooks", WebhookController, { middleware: ["require-api-key"] });

// Use on a group
router.group("/admin", (r) => {
  r.get("/dashboard", DashboardController);
  r.delete("/users/:id", DeleteUserController);
}, { middleware: ["auth", "can:admin"] });

Middleware Execution Order

  1. Global middleware (in registration order)
  2. Route / group middleware (in array order)
  3. Controller beforeHandle() hook
  4. Controller handle()
  5. Controller afterHandle() hook
  6. Middleware after() hooks (reverse order)

CORS Middleware

Enable CORS for browser clients:

import { createCorsMiddleware } from "katal";

app.use(createCorsMiddleware({
  origin:          ["https://app.example.com", "https://www.example.com"],
  methods:         ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders:  ["Content-Type", "Authorization"],
  credentials:     true,
}));

CorsOptions

Option Type Default Description
origin string \| string[] "*" Allowed origin(s)
methods string[] common verbs Allowed HTTP methods
allowedHeaders string[] ["Content-Type","Authorization"] Allowed request headers
exposedHeaders string[] [] Headers exposed to the browser
credentials boolean false Include Access-Control-Allow-Credentials
maxAge number 86400 Preflight cache duration in seconds

Security note: Avoid origin: "*" combined with credentials: true. Pick one.


Rate Limit Middleware

Throttle requests per client. See Rate Limiting for the full reference.

import { createRateLimitMiddleware } from "katal";

app.use(createRateLimitMiddleware({
  windowMs:    60_000, // 1 minute
  maxRequests: 100,
}));

Auth Middleware

Authenticate JWT tokens. See Authentication.

app.middleware("auth", createAuthMiddleware(auth));
app.middleware("auth:optional", createOptionalAuthMiddleware(auth));

Custom State Middleware

Use ctx.state to pass data between middleware and controllers:

app.middleware("tenant", {
  async before(ctx) {
    const tenantId = ctx.request.headers.get("X-Tenant-Id");
    if (!tenantId) return new Response("Missing tenant", { status: 400 });
    ctx.state.tenant = await db.getTenant(tenantId);
    return null;
  },
});

// In controller:
const tenant = ctx.state.tenant as Tenant;

Timing / Observability Middleware

Record latency with before + after:

app.use({
  async before(ctx) {
    ctx.state.startedAt = Date.now();
    return null;
  },
  async after(ctx, response) {
    const ms = Date.now() - (ctx.state.startedAt as number);
    const headers = new Headers(response.headers);
    headers.set("X-Response-Time", `${ms}ms`);
    return new Response(response.body, { status: response.status, headers });
  },
});

See Also