middleware
Wrap a middleware handler with configuration options. Middleware files (middleware.ts) are placed in route directories and automatically collected by the file-based router. They run before a request reaches a page or endpoint.
A bare export default works for simple middleware — the middleware() wrapper is only needed when you want to configure inheritance behavior.
Import
import { middleware } from "catmint/middleware";
Signature
function middleware(
handler: (
req: Request,
next: () => Promise<Response>,
) => Promise<Response> | Response,
options?: MiddlewareOptions,
): MiddlewareHandler;
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
handler | (req: Request, next: () => Promise<Response>) => Promise<Response> | Response | Yes | The middleware function. Receives the request and a next function to pass control to the next handler. |
options | MiddlewareOptions | No | Middleware configuration options. |
MiddlewareOptions
interface MiddlewareOptions {
inherit?: boolean;
name?: string;
}
| Field | Type | Default | Description |
|---|---|---|---|
inherit | boolean | true | Whether to run parent middleware first. Set to false to start a new middleware chain. |
name | string | — | Explicit name for debugging and dev tooling. |
Return Value
Returns a MiddlewareHandler — the handler function with attached __catmintMiddleware metadata.
type MiddlewareHandler = ((
req: Request,
next: () => Promise<Response>,
) => Promise<Response> | Response) & {
__catmintMiddleware?: MiddlewareOptions;
};
Resolution and Execution
Resolution (tip-to-root): Catmint walks from the matched route's directory up to app/, collecting middleware.ts files. If any middleware uses { inherit: false }, ancestor collection stops.
Execution (root-to-tip): The outermost middleware runs first. Each middleware either:
- Calls
next()— passes control to the next handler. The return value is theResponsefrom downstream, which can be inspected or modified. - Returns a
Responsedirectly — short-circuits the chain. All remaining middleware and the route handler are bypassed.
A (root) → B (dashboard) → handler
A runs → calls next() → B runs → calls next() → handler
← Response
← B can modify response
← A can modify response
→ Response sent to client
Compilation Model
At build time, Catmint collects all middleware.ts files and compiles them into the SSR entry module. The build process:
- Scans
app/for allmiddleware.tsfiles and records their directory paths - Generates imports for each middleware in the SSR entry (
ssr-entry.js) - Builds a
middlewareMap— a mapping from directory paths to their middleware handler functions - Exports
executeMiddleware(url, request)— a function that resolves the applicable middleware chain for a given URL, composes them in root-to-tip order (respectinginherit: falseboundaries), and runs them
The next() call is sugar for invoking the next handler directly — there is no runtime middleware resolution or dynamic dispatch. The build also sets a hasMiddleware flag in the manifest, which adapters use to determine whether middleware execution is needed.
In production, each adapter calls executeMiddleware() from the SSR entry for incoming requests. If no middleware files exist in the project, the middleware pipeline is skipped entirely.
Examples
// app/middleware.ts — runs for every request
export default async function (req: Request, next: () => Promise<Response>) {
const start = Date.now();
const response = await next();
response.headers.set("X-Response-Time", `${Date.now() - start}ms`);
return response;
}
// app/dashboard/middleware.ts — auth check
import { middleware } from "catmint/middleware";
import { redirect } from "catmint/routing";
export default middleware(
async (req, next) => {
const session = await getSession(req);
if (!session) {
redirect("/login");
}
return next();
},
{ name: "dashboard-auth" },
);
// app/api/public/middleware.ts — standalone (no parent middleware)
import { middleware } from "catmint/middleware";
export default middleware(
async (req, next) => {
if (isRateLimited(req)) {
return Response.json({ error: "Too many requests" }, { status: 429 });
}
return next();
},
{ inherit: false },
);
Directory Structure
app/
├── middleware.ts # Runs for ALL requests (logging, CORS)
├── dashboard/
│ ├── middleware.ts # Auth check — inherits root middleware
│ └── page.tsx # root → dashboard → page
├── api/
│ ├── middleware.ts # API auth — inherits root middleware
│ └── public/
│ ├── middleware.ts # { inherit: false } — rate limit only
│ └── health/
│ └── endpoint.ts # public middleware only (no root or api middleware)
