Middleware
Astro middleware intercepts requests and responses, runs before page rendering, and can share data across the request lifecycle via locals.
Basic middleware
Create src/middleware.ts:
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware((context, next) => {
// Runs before every page/endpoint
console.log(`Request: ${context.url.pathname}`);
return next();
});
Modifying the response
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Add headers to every response
response.headers.set('X-Custom-Header', 'value');
return response;
});
Redirects
export const onRequest = defineMiddleware((context, next) => {
if (context.url.pathname === '/daily/today') {
const today = new Date().toISOString().split('T')[0];
return context.redirect(`/daily/${today}`);
}
return next();
});
Sharing data with locals
Middleware can attach data to context.locals, accessible in pages and endpoints:
// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
const token = context.cookies.get('session')?.value;
if (token) {
context.locals.user = await verifyToken(token);
}
return next();
});
---
// src/pages/dashboard.astro
const user = Astro.locals.user;
if (!user) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
Type locals in src/env.d.ts:
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
user?: { id: string; name: string; };
}
}
Chaining middleware
Use sequence() to compose multiple middleware:
import { defineMiddleware, sequence } from 'astro:middleware';
const auth = defineMiddleware(async (context, next) => {
// auth logic
return next();
});
const logging = defineMiddleware(async (context, next) => {
const start = Date.now();
const response = await next();
console.log(`${context.url.pathname} - ${Date.now() - start}ms`);
return response;
});
export const onRequest = sequence(auth, logging);
Static vs on-demand behavior
- Static pages: middleware runs at build time only
- On-demand pages: middleware runs per request
If you need middleware to run per request (auth, redirects, logging), the relevant routes must be on-demand (prerender = false).
Comparison with Next.js middleware
| Feature | Next.js | Astro |
|---|---|---|
| Location | middleware.ts at project root | src/middleware.ts |
| Runs on | Edge runtime (limited APIs) | Same runtime as pages |
| Data sharing | Headers/cookies | context.locals (typed) |
| Composable | Single middleware function | sequence() for chaining |
| Static pages | Runs per request (edge) | Runs at build time only |
| Matcher | config.matcher pattern | Check context.url manually |
Key advantage of Astro middleware: it runs in the same Node.js runtime as your pages, so you have full access to Node APIs, databases, and filesystem. Next.js middleware runs on the Edge runtime with significant limitations.