Middleware
Middleware
Route-level middleware lets you intercept page requests before they reach the page handler. Use it for authentication, redirects, logging, and validation.
The _middleware.ts convention
Place a _middleware.ts file in any route directory. It runs for all routes in that directory and its subdirectories.
pages/_middleware.ts -> runs for all routesindex.tsxdashboard/_middleware.ts -> runs only for /dashboard/*settings.tsxprofile.tsxadmin/_middleware.ts -> auth check for /admin/*index.tsx
Middleware context
A middleware function receives a context object with params (dynamic route params) and request (the original Request), plus a next function as the second argument to continue the chain. It returns a Response.
import type { MiddlewareContext, MiddlewareNext } from "@thexjs/core";export async function middleware(ctx: MiddlewareContext, next: MiddlewareNext) { console.log(`[${ctx.request.method}] ${ctx.request.url}`); return next();}Auth middleware example
A common use case is checking for an auth cookie and redirecting unauthenticated users.
import type { MiddlewareContext, MiddlewareNext } from "@thexjs/core";export async function middleware(ctx: MiddlewareContext, next: MiddlewareNext) { const session = ctx.request.headers.get("cookie"); if (!session) { return new Response(null, { status: 302, headers: { Location: "/login" }, }); } const user = await validateSession(session); if (!user) { return new Response(null, { status: 302, headers: { Location: "/login" }, }); } return next();}MiddlewareNext
Call next() (no arguments) to pass control to the next middleware or the route handler. Any mutations to ctx.params you make before the call flow through to downstream handlers.
Middleware applies to page routes only. API routes (in apiDir) and content routes are dispatched without a middleware chain.
Route-level export const middleware
Instead of a separate _middleware.ts file, you can export middleware directly from a page file. This is useful when the middleware is short and tightly coupled to one route.
import type { RouteProps, MiddlewareContext, MiddlewareNext } from "@thexjs/core";export async function middleware(ctx: MiddlewareContext, next: MiddlewareNext) { const session = ctx.request.headers.get("cookie"); if (!session) return new Response(null, { status: 302, headers: { Location: "/login" } }); return next();}export default function Dashboard({}: RouteProps) { return <h1>Welcome back</h1>;}When both a _middleware.ts file and an inline export const middleware exist in the same directory, the file-level middleware runs first, then the route-level one. The same MiddlewareContext and MiddlewareNext types apply to both variants.
composeMiddleware
The _middleware.ts convention builds a single handler under the hood, but you can also wire middleware yourself with composeMiddleware(fns, handler). It folds an array of middleware functions into one onion-style chain that reaches handler only after every next() has been called.
import { composeMiddleware } from "@thexjs/core";import type { MiddlewareContext } from "@thexjs/core";function enforceAuth(ctx: MiddlewareContext, next: () => Promise<Response>) { const session = ctx.request.headers.get("cookie"); if (!session) return new Response(null, { status: 302, headers: { Location: "/login" } }); return next();}function attachUser(ctx: MiddlewareContext, next: () => Promise<Response>) { ctx.params.role = "viewer"; return next();}const wrapped = composeMiddleware( [enforceAuth, attachUser], (ctx) => new Response(`role: ${"$"}{ctx.params.role}`),);Calling next() more than once in the same middleware throws ("next() called multiple times"), which catches double-dispatch bugs at runtime.
Redirect patterns
Return a Response with a 302 status and a Location header to redirect. You can also return JSON responses for API middleware validation errors.
// Redirect to loginreturn new Response(null, { status: 302, headers: { Location: "/login?redirect=" + ctx.request.url },});// Redirect back after successful authconst url = new URL(ctx.request.url);const redirectTo = url.searchParams.get("redirect") || "/";return new Response(null, { status: 302, headers: { Location: redirectTo },});Composing middleware
Multiple folder middleware files run onion-style in directory order. To chain middleware inside a single file, use composeMiddleware from @thexjs/core:
import { composeMiddleware } from "@thexjs/core";export const middleware = composeMiddleware( [withLogging, withAuth], async (ctx) => renderPage(ctx),);