Astro Middleware
Middleware patterns in Astro for authentication, request modification, response headers, and shared context
You are an expert in Astro middleware for intercepting requests, handling authentication, and sharing data between middleware and pages. ## Key Points - Use `sequence()` to compose focused, single-responsibility middleware functions rather than one large middleware. - Type `App.Locals` in `src/env.d.ts` so that every page and endpoint has autocompletion for locals. - Keep middleware fast. Heavy operations (database queries, external API calls) should be cached or deferred when possible. - Only run authentication checks on routes that need them. Check the pathname early and return `next()` for public routes to avoid unnecessary work. - Use middleware for cross-cutting concerns (auth, headers, logging, i18n) and keep business logic in pages and endpoints. - **Forgetting to return `next()` or a Response**: If middleware neither calls `next()` nor returns a Response, the request hangs indefinitely. - **Mutating the response body**: You cannot easily read and modify the HTML body of a response in middleware. Use `astro:before-swap` on the client or an integration hook for HTML transformations. - **Not handling CORS preflight**: For API routes, `OPTIONS` requests need an explicit handler. Middleware can catch these, but you must return the right headers before calling `next()`. - **Rate limiting in serverless**: The in-memory rate limiter shown above resets on every cold start. For serverless deployments, use an external store like Redis or a platform-provided rate limiter. - **Middleware ordering with `sequence`**: Middleware runs in the order listed. If the auth middleware depends on a request-ID set by the logging middleware, make sure logging comes first.
skilldb get astro-skills/Astro MiddlewareFull skill: 355 linesMiddleware — Astro
You are an expert in Astro middleware for intercepting requests, handling authentication, and sharing data between middleware and pages.
Overview
Astro middleware lets you intercept requests and responses on every page load and API endpoint call. Middleware runs on the server before your page or endpoint code and can modify the request, set shared data via locals, redirect unauthenticated users, add response headers, and more. Middleware is defined in src/middleware.ts.
Core Concepts
Basic Middleware
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
// Runs before the page/endpoint handler
console.log(`Request: ${context.request.method} ${context.url.pathname}`);
// Call next() to continue to the page/endpoint
const response = await next();
// Runs after the page/endpoint handler
// You can modify the response here
return response;
});
The Context Object
Middleware receives the same context available in pages and endpoints:
export const onRequest = defineMiddleware(async (context, next) => {
context.url; // URL object for the request
context.request; // Standard Request object
context.params; // Dynamic route parameters
context.cookies; // Cookie helper (get, set, delete)
context.locals; // Mutable object shared with pages/endpoints
context.clientAddress; // Client IP address
context.redirect(url); // Return a redirect response
return next();
});
Setting Locals
context.locals is the primary way to pass data from middleware to pages and endpoints:
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const token = context.cookies.get('auth_token')?.value;
if (token) {
try {
const user = await verifyToken(token);
context.locals.user = user;
context.locals.isAuthenticated = true;
} catch {
context.locals.user = null;
context.locals.isAuthenticated = false;
}
} else {
context.locals.user = null;
context.locals.isAuthenticated = false;
}
return next();
});
Access locals in any page or endpoint:
---
// src/pages/dashboard.astro
const { user, isAuthenticated } = Astro.locals;
if (!isAuthenticated) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
Typing Locals
Define the shape of locals for full TypeScript support:
// src/env.d.ts
declare namespace App {
interface Locals {
user: {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
} | null;
isAuthenticated: boolean;
requestId: string;
}
}
Implementation Patterns
Authentication Guard
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
const protectedRoutes = ['/dashboard', '/settings', '/admin'];
export const onRequest = defineMiddleware(async (context, next) => {
const isProtected = protectedRoutes.some(route =>
context.url.pathname.startsWith(route)
);
if (isProtected) {
const token = context.cookies.get('session')?.value;
if (!token) {
return context.redirect(`/login?redirect=${context.url.pathname}`);
}
try {
const user = await verifySession(token);
context.locals.user = user;
} catch {
context.cookies.delete('session');
return context.redirect('/login');
}
}
return next();
});
Role-Based Access Control
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
const roleRoutes: Record<string, string[]> = {
'/admin': ['admin'],
'/editor': ['admin', 'editor'],
};
export const onRequest = defineMiddleware(async (context, next) => {
// Authentication (populate locals.user)
const token = context.cookies.get('session')?.value;
if (token) {
context.locals.user = await verifySession(token);
}
// Authorization
for (const [prefix, allowedRoles] of Object.entries(roleRoutes)) {
if (context.url.pathname.startsWith(prefix)) {
const user = context.locals.user;
if (!user || !allowedRoles.includes(user.role)) {
return new Response('Forbidden', { status: 403 });
}
}
}
return next();
});
Adding Response Headers
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
// Security headers
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
// CORS for API routes
if (context.url.pathname.startsWith('/api/')) {
response.headers.set('Access-Control-Allow-Origin', 'https://example.com');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
return response;
});
Request Logging
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import crypto from 'node:crypto';
export const onRequest = defineMiddleware(async (context, next) => {
const requestId = crypto.randomUUID();
context.locals.requestId = requestId;
const start = performance.now();
const response = await next();
const duration = Math.round(performance.now() - start);
console.log(
JSON.stringify({
requestId,
method: context.request.method,
path: context.url.pathname,
status: response.status,
duration: `${duration}ms`,
userAgent: context.request.headers.get('user-agent'),
})
);
response.headers.set('X-Request-Id', requestId);
return response;
});
Middleware Chaining with sequence
Compose multiple middleware functions using sequence:
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';
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;
});
const auth = defineMiddleware(async (context, next) => {
const token = context.cookies.get('session')?.value;
if (token) {
context.locals.user = await verifySession(token);
}
return next();
});
const i18n = defineMiddleware(async (context, next) => {
const lang = context.cookies.get('lang')?.value
|| context.request.headers.get('accept-language')?.split(',')[0]?.split('-')[0]
|| 'en';
context.locals.lang = lang;
return next();
});
// Runs in order: logging -> auth -> i18n
export const onRequest = sequence(logging, auth, i18n);
Rate Limiting
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
const rateLimit = new Map<string, { count: number; resetAt: number }>();
const WINDOW_MS = 60_000; // 1 minute
const MAX_REQUESTS = 100;
export const onRequest = defineMiddleware(async (context, next) => {
if (!context.url.pathname.startsWith('/api/')) {
return next();
}
const ip = context.clientAddress;
const now = Date.now();
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetAt) {
rateLimit.set(ip, { count: 1, resetAt: now + WINDOW_MS });
} else {
entry.count++;
if (entry.count > MAX_REQUESTS) {
return new Response(JSON.stringify({ error: 'Too many requests' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': String(Math.ceil((entry.resetAt - now) / 1000)),
},
});
}
}
return next();
});
Core Philosophy
Middleware in Astro occupies a deliberate position in the request lifecycle: it runs on the server before any page or endpoint handler, making it the right place for cross-cutting concerns that apply uniformly across routes. Authentication, request logging, security headers, and internationalization are natural fits. Business logic, data transformation, and content rendering are not.
The locals pattern is Astro's answer to dependency injection for the request lifecycle. Rather than passing data through global state or module-scoped variables, middleware attaches request-specific values to context.locals, which flows cleanly into every page and endpoint. This makes the data flow explicit and testable: you can see exactly what each middleware contributes and what each page consumes.
The sequence utility encourages decomposition into small, single-purpose middleware functions. Each function does one thing: check auth, set headers, log timing, detect locale. Composing them with sequence makes the execution order explicit and the behavior of each layer independently verifiable. This is far preferable to a monolithic middleware function that grows to handle dozens of concerns in one place.
Anti-Patterns
-
Performing heavy computation in middleware. Database queries, external API calls, or complex transformations in middleware run on every request, including pre-rendered pages during build. Keep middleware fast and defer expensive work to specific loaders or endpoints.
-
Using middleware for business logic. Middleware should handle infrastructure concerns (auth, headers, logging), not decide what content to show or how to process form data. Route-specific logic belongs in pages and endpoints.
-
Mutating the response body in middleware. Astro middleware can set headers and return redirects but is not designed for HTML transformation. Attempting to read and rewrite the response body leads to complex, fragile code. Use integration hooks or client-side events for HTML manipulation.
-
Forgetting to call
next()or return a Response. A middleware that neither callsnext()nor returns a Response causes the request to hang indefinitely. Every code path must terminate with one or the other. -
Applying auth checks to every route indiscriminately. Running authentication logic on public pages, static assets, and API health checks wastes resources. Check the pathname early and return
next()for routes that do not need protection.
Best Practices
- Use
sequence()to compose focused, single-responsibility middleware functions rather than one large middleware. - Type
App.Localsinsrc/env.d.tsso that every page and endpoint has autocompletion for locals. - Keep middleware fast. Heavy operations (database queries, external API calls) should be cached or deferred when possible.
- Only run authentication checks on routes that need them. Check the pathname early and return
next()for public routes to avoid unnecessary work. - Use middleware for cross-cutting concerns (auth, headers, logging, i18n) and keep business logic in pages and endpoints.
Common Pitfalls
- Middleware runs on pre-rendered pages at build time: During
astro build, middleware executes for pre-rendered pages. Avoid relying on runtime-only features (like real cookies or IP addresses) for static pages. - Forgetting to return
next()or a Response: If middleware neither callsnext()nor returns a Response, the request hangs indefinitely. - Mutating the response body: You cannot easily read and modify the HTML body of a response in middleware. Use
astro:before-swapon the client or an integration hook for HTML transformations. - Not handling CORS preflight: For API routes,
OPTIONSrequests need an explicit handler. Middleware can catch these, but you must return the right headers before callingnext(). - Rate limiting in serverless: The in-memory rate limiter shown above resets on every cold start. For serverless deployments, use an external store like Redis or a platform-provided rate limiter.
- Middleware ordering with
sequence: Middleware runs in the order listed. If the auth middleware depends on a request-ID set by the logging middleware, make sure logging comes first.
Install this skill directly: skilldb add astro-skills
Related Skills
Astro Basics
Astro fundamentals including project structure, components, islands architecture, and templating syntax
Astro Content Collections
Content collections in Astro for managing Markdown, MDX, JSON, and YAML content with type-safe schemas
Astro Deployment
Deploying Astro sites to Vercel, Netlify, Cloudflare Pages, and other platforms
Astro Integrations
Using React, Vue, Svelte, and other UI framework islands within Astro pages
Astro Routing
File-based and dynamic routing in Astro including static paths, rest parameters, and route priority
Astro SSR
Server-side rendering in Astro with adapters for Node, Vercel, Netlify, Cloudflare, and Deno