Middleware in Astro allows you to intercept requests and responses, modify data, add authentication, log requests, and perform other server-side operations before rendering pages.
Basic Middleware
Create middleware by defining a middleware.ts file in your src/ directory:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware (( context , next ) => {
console . log ( 'Request:' , context . request . url );
// Continue to the next middleware or route
return next ();
});
Middleware runs on every request in SSR mode, and during build time for pre-rendered pages.
The Context Object
Middleware receives a context object with request information and helpful utilities:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware (( context , next ) => {
// Access request properties
console . log ( 'URL:' , context . url . pathname );
console . log ( 'Method:' , context . request . method );
console . log ( 'Params:' , context . params );
// Access cookies
const userId = context . cookies . get ( 'userId' );
// Access client IP
const ip = context . clientAddress ;
// Modify locals (shared with pages)
context . locals . user = { id: userId };
return next ();
});
Available Properties
The incoming HTTP request
Route parameters from dynamic routes
Cookie utilities for reading/writing cookies
Shared data object passed to pages
redirect
(path: string, status?: number) => Response
Helper to create redirect responses
rewrite
(path: string) => Promise<Response>
Internally rewrite to a different URL
Authentication Example
Implement authentication in middleware:
import { defineMiddleware } from 'astro:middleware' ;
import { verifySession } from './lib/auth' ;
export const onRequest = defineMiddleware ( async ( context , next ) => {
const sessionId = context . cookies . get ( 'session' )?. value ;
if ( ! sessionId ) {
// No session - allow unauthenticated access
context . locals . user = null ;
return next ();
}
try {
// Verify session and get user
const user = await verifySession ( sessionId );
context . locals . user = user ;
return next ();
} catch ( error ) {
// Invalid session - clear cookie and redirect
context . cookies . delete ( 'session' );
return context . redirect ( '/login' );
}
});
Access the user in your pages:
src/pages/dashboard.astro
---
const { user } = Astro . locals ;
if ( ! user ) {
return Astro . redirect ( '/login' );
}
---
< html >
< body >
< h1 > Welcome, { user . name } ! </ h1 >
</ body >
</ html >
Use context.locals to share data between middleware and your pages. Type it by creating src/env.d.ts.
Sequencing Middleware
Chain multiple middleware functions using sequence:
import { sequence } from 'astro:middleware' ;
import { auth } from './middleware/auth' ;
import { logger } from './middleware/logger' ;
import { cors } from './middleware/cors' ;
export const onRequest = sequence (
logger ,
cors ,
auth
);
Each middleware in the sequence:
import { defineMiddleware } from 'astro:middleware' ;
export const logger = defineMiddleware (( context , next ) => {
const start = Date . now ();
console . log ( `[ ${ new Date (). toISOString () } ] ${ context . request . method } ${ context . url . pathname } ` );
const response = next ();
response . then (() => {
const duration = Date . now () - start ;
console . log ( `Completed in ${ duration } ms` );
});
return response ;
});
import { defineMiddleware } from 'astro:middleware' ;
export const cors = defineMiddleware ( async ( context , next ) => {
const response = await next ();
response . headers . set ( 'Access-Control-Allow-Origin' , '*' );
response . headers . set ( 'Access-Control-Allow-Methods' , 'GET, POST, OPTIONS' );
return response ;
});
Middleware executes in the order specified:
logger runs first
cors runs second
auth runs last
Page handler executes
Response bubbles back through middleware
Any middleware can return early, skipping subsequent middleware: export const auth = defineMiddleware (( context , next ) => {
if ( ! context . locals . user ) {
// Skip remaining middleware and redirect
return context . redirect ( '/login' );
}
return next ();
});
Modifying Responses
Modify responses before they’re sent:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware ( async ( context , next ) => {
const response = await next ();
// Clone response to modify headers
const modifiedResponse = new Response ( response . body , {
status: response . status ,
statusText: response . statusText ,
headers: response . headers
});
// Add custom headers
modifiedResponse . headers . set ( 'X-Custom-Header' , 'value' );
modifiedResponse . headers . set ( 'X-Response-Time' , String ( Date . now ()));
return modifiedResponse ;
});
Redirects and Rewrites
Redirects
Return redirect responses to send users to different URLs:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware (( context , next ) => {
// Redirect old URLs to new ones
if ( context . url . pathname === '/old-page' ) {
return context . redirect ( '/new-page' , 301 );
}
// Redirect based on user state
if ( context . url . pathname . startsWith ( '/admin' ) && ! context . locals . user ?. isAdmin ) {
return context . redirect ( '/forbidden' , 403 );
}
return next ();
});
Rewrites
Internally rewrite requests to different routes without changing the URL:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware ( async ( context , next ) => {
// Rewrite API versioning
if ( context . url . pathname . startsWith ( '/api/v1' )) {
return context . rewrite ( context . url . pathname . replace ( '/v1' , '/v2' ));
}
// Rewrite based on user preferences
if ( context . locals . user ?. locale === 'fr' ) {
return context . rewrite ( `/fr ${ context . url . pathname } ` );
}
return next ();
});
Rewrites keep the original URL in the browser while serving different content. Use redirects to change the browser URL.
Cookie Management
Work with cookies in middleware:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware (( context , next ) => {
// Read cookies
const theme = context . cookies . get ( 'theme' )?. value ?? 'light' ;
const consent = context . cookies . get ( 'consent' );
// Set cookies
context . cookies . set ( 'last-visit' , new Date (). toISOString (), {
path: '/' ,
httpOnly: true ,
secure: true ,
maxAge: 60 * 60 * 24 * 365 // 1 year
});
// Delete cookies
if ( context . url . pathname === '/logout' ) {
context . cookies . delete ( 'session' );
}
context . locals . theme = theme ;
return next ();
});
Cookie Options
Cookie path. Default: ’/’
HTTP-only flag (not accessible via JavaScript)
sameSite
'strict' | 'lax' | 'none'
SameSite attribute
Type Safety
Type your locals object for better TypeScript support:
/// < reference types = "astro/client" />
declare namespace App {
interface Locals {
user : {
id : string ;
name : string ;
email : string ;
isAdmin : boolean ;
} | null ;
theme : 'light' | 'dark' ;
}
}
Now context.locals and Astro.locals are fully typed:
import { defineMiddleware } from 'astro:middleware' ;
export const onRequest = defineMiddleware (( context , next ) => {
// TypeScript knows the shape of locals
context . locals . user = {
id: '123' ,
name: 'John' ,
email: '[email protected] ' ,
isAdmin: false
};
return next ();
});
API Routes
Middleware also runs for API routes:
import type { APIRoute } from 'astro' ;
export const GET : APIRoute = ( context ) => {
// Access data from middleware
const user = context . locals . user ;
if ( ! user ) {
return new Response ( JSON . stringify ({ error: 'Unauthorized' }), {
status: 401 ,
headers: { 'Content-Type' : 'application/json' }
});
}
return new Response ( JSON . stringify ({ data: 'secret' }), {
headers: { 'Content-Type' : 'application/json' }
});
};
Common Patterns
Rate Limiting
Localization
src/middleware/rate-limit.ts
import { defineMiddleware } from 'astro:middleware' ;
const requests = new Map < string , number []>();
export const rateLimit = defineMiddleware (( context , next ) => {
const ip = context . clientAddress ;
const now = Date . now ();
const windowMs = 60 * 1000 ; // 1 minute
const maxRequests = 100 ;
const userRequests = requests . get ( ip ) || [];
const recentRequests = userRequests . filter ( time => now - time < windowMs );
if ( recentRequests . length >= maxRequests ) {
return new Response ( 'Too many requests' , { status: 429 });
}
recentRequests . push ( now );
requests . set ( ip , recentRequests );
return next ();
});
import { defineMiddleware } from 'astro:middleware' ;
const supportedLocales = [ 'en' , 'fr' , 'es' ];
export const i18n = defineMiddleware (( context , next ) => {
// Get locale from URL or cookies
const urlLocale = context . url . pathname . split ( '/' )[ 1 ];
const cookieLocale = context . cookies . get ( 'locale' )?. value ;
const locale = supportedLocales . includes ( urlLocale )
? urlLocale
: cookieLocale || 'en' ;
context . locals . locale = locale ;
context . cookies . set ( 'locale' , locale );
return next ();
});
Conditional Middleware
Run middleware conditionally based on the route:
import { defineMiddleware , sequence } from 'astro:middleware' ;
import { auth } from './middleware/auth' ;
import { logger } from './middleware/logger' ;
const conditionalAuth = defineMiddleware (( context , next ) => {
// Only run auth on protected routes
if ( context . url . pathname . startsWith ( '/dashboard' )) {
return auth ( context , next );
}
return next ();
});
export const onRequest = sequence (
logger ,
conditionalAuth
);
Actions Handle form submissions
API Routes Build API endpoints
Authentication Authentication patterns