Middleware
Middleware in WebJs lets you intercept requests before they reach your pages, API routes, or server actions. Use it for authentication, logging, rate limiting, CORS, header injection, or any cross-cutting concern. WebJs supports two levels of middleware: a single root middleware and per-segment middleware scoped to subtrees of your route hierarchy.
Root Middleware
Place a middleware.ts at the root of your project (next to app/, not inside it). This middleware runs on every request before WebJs routes it to a page, API route, or server action. Any of middleware.ts, .js, .mts, or .mjs works, and .ts wins if you somehow have more than one.
my-app/
middleware.ts # root middleware: runs on every request
app/
page.ts
api/
hello/
route.ts
// middleware.ts
export default async function middleware(
req: Request,
next: () => Promise<Response>,
): Promise<Response> {
const started = Date.now();
const resp = await next();
const elapsed = Date.now() - started;
console.log(`${req.method} ${new URL(req.url).pathname} -> ${resp.status} (${elapsed}ms)`);
resp.headers.set('x-response-time', `${elapsed}ms`);
return resp;
}
Per-Segment Middleware
Place a middleware.ts inside any directory under app/ to scope it to that subtree. It runs only for requests whose URL matches that segment and its children.
my-app/
middleware.ts # root: every request
app/
page.ts # /: root + no segment middleware
dashboard/
middleware.ts # only /dashboard/* requests
page.ts # / dashboard
settings/
page.ts # /dashboard/settings
api/
auth/
middleware.ts # only /api/auth/* requests
login/
route.ts # POST /api/auth/login
signup/
route.ts # POST /api/auth/signup
Signature
Every middleware function has the same signature, whether root or per-segment:
export default async function middleware( req: Request, next: () => Promise<Response>, ): Promise<Response>
- req: a standard Request object. Read headers, cookies, URL, method, body.
- next(): calls the next middleware in the chain, or the final handler (page render, API route, action). Returns a
Promise<Response>. - Return value: you must return a
Response. Either pass through the one fromnext()(optionally modified), or return your own to short-circuit the chain.
The file must export default a function. Named exports are ignored.
Chain Order
When a request arrives, middleware executes in this order:
- Root middleware (
middleware.tsat project root) - Outermost segment middleware (
app/middleware.tsif it exists) - Next segment (
app/dashboard/middleware.ts) - Innermost segment (deepest
middleware.tson the matched route) - Handler (page SSR, API route, or server action)
Each middleware calls next() to proceed. Responses bubble back up through the chain in reverse order, so outer middleware can inspect or modify the final response.
// Execution flow for GET /dashboard/settings: // // root middleware // -> app/dashboard/middleware.ts // -> SSR app/dashboard/settings/page.ts // <- Response // <- Response (dashboard middleware can modify) // <- Response (root middleware can modify)
Short-Circuiting
Return a Response without calling next() to stop the chain early. The request never reaches downstream middleware or the route handler.
// app/api/middleware.ts: require API key for all /api/* routes
export default async function apiAuth(
req: Request,
next: () => Promise<Response>,
): Promise<Response> {
const key = req.headers.get('x-api-key');
if (key !== process.env.API_KEY) {
return Response.json(
{ error: 'Invalid API key' },
{ status: 401 },
);
}
return next();
}
Use Case: Auth Gate on /dashboard
A common pattern: require authentication for an entire subtree by placing a middleware in the segment directory.
// app/dashboard/middleware.ts
import { cookies } from '@webjsdev/server';
import { getUserByToken, SESSION_COOKIE } from '#lib/session.server.ts';
export default async function requireAuth(
req: Request,
next: () => Promise<Response>,
): Promise<Response> {
const user = await getUserByToken(cookies().get(SESSION_COOKIE));
if (!user) {
const to = encodeURIComponent(new URL(req.url).pathname);
return new Response(null, {
status: 302,
headers: { location: `/login?then=${to}` },
});
}
return next();
}
Every page and API route under app/dashboard/ is now protected. Unauthenticated users are redirected to /login with a then query param so they can be sent back after signing in.
Use Case: Logging and Timing
// middleware.ts (root)
export default async function logger(
req: Request,
next: () => Promise<Response>,
): Promise<Response> {
const url = new URL(req.url);
const start = Date.now();
const resp = await next();
const ms = Date.now() - start;
console.log(`${req.method} ${url.pathname} ${resp.status} ${ms}ms`);
return resp;
}
Use Case: CORS Headers
// app/api/middleware.ts: add CORS to all /api/* routes
export default async function cors(
req: Request,
next: () => Promise<Response>,
): Promise<Response> {
// Preflight
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
'access-control-allow-headers': 'content-type, authorization',
'access-control-max-age': '86400',
},
});
}
const resp = await next();
resp.headers.set('access-control-allow-origin', '*');
return resp;
}
WebJs also ships a ready-made cors() middleware (from @webjsdev/server) you can wrap around a single route.ts handler. Use middleware CORS when you need blanket coverage across all routes in a segment.
Rate Limiting
WebJs ships a built-in rate limiter as a middleware factory. Import rateLimit from @webjsdev/server:
// app/api/auth/middleware.ts
import { rateLimit } from '@webjsdev/server';
export default rateLimit({ window: '10s', max: 5 });
That single line protects every route under /api/auth/ (login, signup, password reset) with a limit of 5 requests per 10 seconds per IP address.
rateLimit() Options
rateLimit({
window: '1m', // time window: number (ms) or string: '30s', '1m', '1h'
max: 60, // max requests per window per key
key: req => { // custom key function (default: IP from x-forwarded-for)
return `login:${req.headers.get('x-forwarded-for') || 'anon'}`;
},
message: 'Slow down' // custom 429 error message
})
The rate limiter is in-memory and uses a fixed-window algorithm. Response headers are set automatically:
x-ratelimit-limit: the max for this windowx-ratelimit-remaining: requests left in the current windowx-ratelimit-reset: unix timestamp when the window resetsretry-after: seconds until the window resets (on 429 responses only)
For multi-instance deployments, rate-limit at the edge (nginx, Cloudflare, AWS WAF) or use the key function to integrate with a shared store like Redis.
cookies() and headers() Helpers
WebJs provides request-scoped helpers via @webjsdev/server that let you read cookies and headers from anywhere in your server-side code (middleware, pages, server actions, API routes) without explicitly threading the request object:
import { cookies, headers } from '@webjsdev/server';
// In any server-side function:
const token = cookies().get('session_token');
const hasToken = cookies().has('session_token');
const allCookies = cookies().entries(); // [string, string][]
const auth = headers().get('authorization');
const userAgent = headers().get('user-agent');
These are backed by AsyncLocalStorage. The request context is established before your middleware runs, so they work everywhere in the request lifecycle. Calling them outside a request scope (e.g., at module top level) throws an error.
Note: cookies() is read-only. To set a cookie, include a Set-Cookie header on the Response you return from your middleware, API route, or server action.
Middleware and Server Actions
Root middleware runs on server action RPC calls (POST /__webjs/action/:hash/:fn) just like any other request. Per-segment middleware does not apply to server actions (they bypass the file-based route tree). For action-level guards, check auth inside the action itself, declare per-action middleware on the action, or call the action from a route.ts under a middleware-protected segment.
Middleware and API Routes
Per-segment middleware applies to API routes (route.ts) within the same subtree. If app/api/middleware.ts exists, it runs before app/api/hello/route.ts, app/api/auth/login/route.ts, and every other route under /api/.
Middleware chains nest: a request to /api/auth/login runs the root middleware, then app/api/middleware.ts, then app/api/auth/middleware.ts, then the route handler.
Tips
- Keep middleware fast. It runs on every request in its scope. Defer heavy work to the route handler when possible.
- Avoid mutating the request. The Web
RequestAPI is largely immutable. If you need to pass data downstream (e.g., a resolved user object), store it in a module-scopedAsyncLocalStorageor use a header. - One default export. Each
middleware.tsmust export a single default function. Multiple middleware in one file are not supported. If you need composition, chain them manually inside your export. - Use
rateLimit()from@webjsdev/serverrather than writing your own. It handles cleanup, header injection, and per-bucket IP resolution that defaults to the framework-stamped socket address (spoof-safe) and only honoursX-Forwarded-For/CF-Connecting-IP/X-Real-IPwhen you opt in withtrustProxy: true. See Rate limiting for the threat model.