Middleware
Routa middleware is part of the contract, not a side effect. Each middleware declares the context keys it requires, the context it provides, and the early responses it can return. Auth, tenant loading, and request enrichment stay visible in types, in generated metadata, and in OpenAPI.
import { createMiddleware } from "@routa-ts/core";import { z } from "zod";
export const withAuth = createMiddleware({ requires: ["session"], provides: { auth: z.object({ userId: z.string(), }), }, rejects: { unauthorized: { status: 401, schema: z.object({ message: z.string() }), }, }, run: ({ ctx, next }) => { if (!ctx.session.authenticated || !ctx.session.userId) { return { type: "unauthorized", data: { message: "Authentication required." } }; }
return next({ auth: { userId: ctx.session.userId } }); },});How It Works
Section titled “How It Works”requires lists context keys that must already exist. Routa checks the order statically,
so a middleware that requires session before anything provides it fails
routa check with ROUTA_MIDDLEWARE_ORDER rather than at request time.
provides maps new context keys to schemas. Calling next({ auth }) parses every declared
value before Routa merges any of them into ctx, so handlers receive transformed schema
outputs and never a partial update. Undeclared top-level keys are omitted. A normal
z.object() strips unknown keys inside its value; add .strict() when those nested keys
must be rejected. This validation runs in both routa dev and routa start, independent
of the response-validation policy.
rejects declares early responses. Returning { type: "unauthorized", data } ends the
request with the declared status and skips the rest of the chain, including the handler.
Those outcomes are merged into the route’s response map, so they are type-checked in
handlers and included in generated OpenAPI. Their response data follows the same runtime
response-validation policy as handler responses.
input is optional and works exactly like a route’s input. It lets middleware read
validated headers, cookies, or query values without the route declaring them:
export const withRequest = createMiddleware({ input: { headers: z.object({ "x-request-id": z.string().optional(), }), }, provides: { requestId: z.string(), }, run: async ({ input, next }) => { return next({ requestId: input.headers["x-request-id"] ?? "local-request" }); },});Where Middleware Applies
Section titled “Where Middleware Applies”Folder Inheritance
Section titled “Folder Inheritance”A middleware.ts file applies to every route in its folder and below. Export or re-export
middleware contracts from it; the export order is the execution order.
export { withRequest, withSession } from "../middleware/context.js";Context accumulates as routing gets more specific, which is why a group folder is a useful place to attach a chain:
Directorysrc/routes/
- middleware.ts
withRequest,withSession Directory(private)/
- middleware.ts
withAuth Directoryadmin/
- middleware.ts
withAdmin Directoryaudit-events/
- route.ts sees requestId, session, auth, admin
- middleware.ts
Directorytenants/
Directory$tenantId/
- middleware.ts
withTenant Directoryprojects/
- route.ts sees requestId, session, auth, tenant
- middleware.ts
- middleware.ts
- middleware.ts
Per Route and Per Method
Section titled “Per Route and Per Method”A route file can add middleware for all of its methods, or one method can add its own:
export default route({ middleware: [withRateLimit], get: createRoute({ middleware: [withReportAccess], // ... }), post: createRoute({ // runs withRateLimit, but not withReportAccess // ... }),});The full chain for a method is folder middleware, then route-level middleware, then method-level middleware.
Security Visibility
Section titled “Security Visibility”Middleware can describe what an operation requires without Routa implementing any of it:
createMiddleware({ openapi: { security: [{ bearerAuth: [] }], permissions: ["audit.read"], }, // Application-owned credential and permission logic in run.});Routa emits security and x-routa-authz on every operation the middleware covers, and
routa openapi breaking reports when a previously public operation becomes authenticated.
- Every reject name must be unique across a method’s whole chain. Two middleware both
declaring
forbiddenon the same route is a registration error. - Reject statuses must be 4xx or 5xx (
ROUTA_MIDDLEWARE_REJECT_STATUS). - Always
awaitor returnnext(). Calling it without awaiting drops the downstream result and can leave the request hanging. next()may be called at most once per middleware invocation.- Middleware must be referenced as a direct identifier of a
createMiddlewareexport. Spreading an array or computing the value at runtime producesROUTA_MIDDLEWARE_UNRESOLVED, because Routa could not check the contract.
Limits
Section titled “Limits”Routa makes security context and early responses visible at the HTTP boundary. Your application still owns authorization rules, policy correctness, credential handling, and secrets. Routa never authenticates a request for you.