Middleware in Anvil is code that runs before (and optionally after) your route handler. You place it in a _middleware.ts file next to the routes it should affect — no central app.use() call, no manual import lists. Anvil wires it up automatically based on location.
The _middleware.ts convention
A file named _middleware.ts in any server/routes/ folder runs for every route under that folder. Root-level middleware (server/routes/_middleware.ts) runs for every request in the application, including unmatched paths (so static files and the observability dashboard can be served from there).
server/routes/
_middleware.ts ← runs for all routes + unmatched paths
get.ts
users/
_middleware.ts ← runs only for /users/**
get.ts
post.ts
[id]/
get.ts
Root middleware example
The root _middleware.ts is a great place for logging, static file serving, and the built-in observability dashboard:
server/routes/_middleware.ts
import { serveStatic, type Middleware } from 'anvil';
const logger: Middleware = async (ctx, next) => {
const start = Date.now();
const res = await next();
console.log(`${ctx.method} ${ctx.path} → ${res.status} (${Date.now() - start}ms)`);
return res;
};
// Root middleware also runs for unmatched paths, so static files
// under public/ are served without needing a dedicated route.
export default [logger, serveStatic({ dir: 'public' })];
Exporting an array of middleware functions is supported. Anvil flattens single functions and arrays — you can mix and match in the same file.
The onion (compose) model
Middleware wraps the handler in an “onion” pattern. Root middleware is the outermost layer; the middleware closest to the handler file runs innermost. Each function receives ctx and a next function to call the next layer.
Request
└─ root _middleware (outermost)
└─ /users _middleware
└─ route handler (innermost)
← response flows back out through each layer
Calling next() passes control inward. You can run code before the handler (pre-processing) and after it (post-processing):
server/routes/users/_middleware.ts
import type { Middleware } from 'anvil';
const scope: Middleware = async (ctx, next) => {
// Pre-processing: runs before the handler
console.log(`Users scope: ${ctx.method} ${ctx.path}`);
const res = await next(); // ← call the next layer
// Post-processing: runs after the handler returns
res.headers.set('x-scope', 'users');
return res;
};
export default scope;
Composing multiple middleware functions
To apply several middleware functions to the same scope, export them as an array from _middleware.ts. Anvil flattens the array and runs each function in order:
server/routes/users/_middleware.ts
import type { Middleware } from 'anvil';
const timer: Middleware = async (ctx, next) => {
const start = Date.now();
const res = await next();
res.headers.set('x-response-time', `${Date.now() - start}ms`);
return res;
};
const scopeHeader: Middleware = async (ctx, next) => {
const res = await next();
res.headers.set('x-scope', 'users');
return res;
};
// Export an array — Anvil applies them outermost-first (timer wraps scopeHeader)
export default [timer, scopeHeader];
The compose() helper (exported from anvil) is used internally by the framework to build the full middleware chain at request time. You can also call it directly when you need to programmatically assemble a handler pipeline — for example, in tests or when creating reusable middleware factories:
import { compose } from 'anvil';
// compose returns a (ctx: Context) => Promise<Response> — a fully-wired handler,
// not a Middleware. Use it when you want to call a pipeline directly, not as a
// _middleware.ts export.
const runChain = compose([timer, scopeHeader], myHandler);
const response = await runChain(ctx);
Passing data with ctx.state
ctx.state is a mutable Record<string, unknown> on every request context. Middleware can write values to it; the downstream handler (or the next middleware) reads them. This replaces the pattern of mutating req in Express.
Auth guard example
server/routes/(authenticated)/_middleware.ts
import { HttpError, type Middleware } from 'anvil';
const authGuard: Middleware = async (ctx, next) => {
const token = ctx.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
throw new HttpError(401, 'Missing Authorization header');
}
// Validate the token — replace this with your real auth logic
const user = await verifyToken(token);
if (!user) {
throw new HttpError(403, 'Invalid or expired token');
}
// Store the authenticated user so handlers can access it
ctx.state.user = user;
return next();
};
export default authGuard;
Read ctx.state.user in any handler under the (authenticated)/ route group:
server/routes/(authenticated)/profile/get.ts
import type { Context } from 'anvil';
export default function handler(ctx: Context) {
// ctx.state.user was set by the auth middleware
const user = ctx.state.user as { id: string; name: string };
return { profile: user };
}
Short-circuiting
A middleware can return a response without calling next() to stop the chain entirely. This is how auth guards, rate limiters, and maintenance-mode middleware work:
server/routes/_middleware.ts
import { type Middleware } from 'anvil';
const maintenanceMode: Middleware = async (ctx, next) => {
if (process.env.MAINTENANCE === 'true') {
// Returns immediately — the handler is never called
return new Response(
JSON.stringify({ error: 'Service temporarily unavailable', status: 503 }),
{ status: 503, headers: { 'content-type': 'application/json' } },
);
}
return next();
};
export default maintenanceMode;
Adding CORS
Anvil ships a cors() middleware helper with the same options as the popular cors npm package. Add it to your root _middleware.ts:
server/routes/_middleware.ts
import { cors, type Middleware } from 'anvil';
const logger: Middleware = async (ctx, next) => {
const start = Date.now();
const res = await next();
console.log(`${ctx.method} ${ctx.path} → ${res.status} (${Date.now() - start}ms)`);
return res;
};
export default [
cors({
origin: ['https://my-frontend.com', 'http://localhost:5173'],
credentials: true,
}),
logger,
];
Available cors() options:
| Option | Type | Default | Description |
|---|
origin | '*' | string[] | (origin: string) => boolean | '*' | Allowed origins. Use an array for an allowlist or a predicate for custom logic. |
methods | string[] | All standard methods | Allowed HTTP methods in preflight responses. |
allowedHeaders | string[] | Reflects request headers | Allowed request headers. |
exposedHeaders | string[] | — | Headers the browser is allowed to read. |
credentials | boolean | false | Set Access-Control-Allow-Credentials: true. |
maxAge | number | — | Preflight cache duration in seconds. |