Docs/Middleware
MIDDLEWARE

Middleware

Middleware functions run before a route handler (page or API route) and have the ability to inspect, modify, or short-circuit a request. They are defined in middleware.ts files placed anywhere in the <project-root>/pages folder.

Defining middleware

Export an async default function that accepts req, res, and next. Call next() to pass control to the next middleware in the chain or to the route handler / the page itself. If you do not call next(), the request is presumed to end. This is useful for auth guards, redirects, or early error responses.

A simple middleware might look like this:

ts
// pages/middleware.ts (runs for every request to any resource within the entire project) import type { IncomingMessage, ServerResponse } from "node:http"; export default async function middleware( req: IncomingMessage, res: ServerResponse, next: () => Promise<void>, ) { console.log(`[${req.method}] ${req.url}`); await next(); }

Scoping middleware

Middleware is scoped by directory, just like layouts. A middleware.ts in pages/ runs for every route; one in pages/admin/ runs only for routes under /admin:

text
pages/ middleware.ts < runs for all routes admin/ middleware.ts < runs only for /admin/* page.ts settings/ page.ts

When a request matches /admin/settings, the chain runs in order:

  1. pages/middleware.ts
  2. pages/admin/middleware.ts
  3. The route handler

Returning Early

If you don't call next(), it's presumed by Elegance that you ended the request.

An early return might look like this:

ts
// pages/admin/middleware.ts export default async function middleware( req: IncomingMessage, res: ServerResponse, next: () => Promise<void>, ) { const token = parseCookie(req.headers.cookie ?? "").token; if (!token || !(await verifyToken(token))) { res.writeHead(302, { Location: "/login" }); res.end(); return; // it's very important to end the request! } await next(); }

Passing Data to the Page

Because IncomingMessage is a plain object, you can attach extra properties before calling next(). Downstream handlers will see them

This is useful de-duplicating DB calls. You can authenticate and get a user from the database, and attach the User object to the request, and if your downstream page / layout is dynamic, the User property will persist.

An example of passing data might look like this:

ts
export default async function middleware( req: IncomingMessage & { user?: User }, res: ServerResponse, next: () => Promise<void>, ) { req.user = await getUserFromSession(req); await next(); }

Error Handling

Wrapping next() in a try/catch to handle errors thrown by downstream handlers can be useful for custom error behavior.

ts
export default async function middleware( req: IncomingMessage, res: ServerResponse, next: () => Promise<void>, ) { try { await next(); } catch (err) { console.error("Unhandled error:", err); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Internal server error" })); } }