Overview
Middleware in Astro allows you to intercept and modify requests and responses before pages are rendered. With Vercel’s Edge Middleware, this processing happens at the edge for optimal performance.
Middleware runs on every request, making it ideal for logging, authentication, redirects, and request modification.
Configuration
Enable Edge Middleware in your Astro config:
import vercel from '@astrojs/vercel'
import { defineConfig } from 'astro/config'
export default defineConfig ({
output: 'server' ,
adapter: vercel ({
edgeMiddleware: true ,
}) ,
})
Edge Middleware runs before your pages and API endpoints, providing a single point for cross-cutting concerns.
Creating Middleware
Create a middleware file at src/middleware.ts:
import type { APIContext , MiddlewareNext } from 'astro'
import { defineMiddleware } from 'astro:middleware'
export const onRequest = defineMiddleware (( context : APIContext , next : MiddlewareNext ) => {
// Intercept data from a request
// Optionally, modify the properties in `locals`
// context.locals.title = 'New Title'
// context.locals.property = 'New Property'
// Log the request
console . log ( context . url . href )
// Return a Response or the result of calling `next()`
return next ()
})
Middleware Context
The APIContext provides access to request information:
// Access the request URL
context . url . pathname // '/api/users'
context . url . href // 'https://example.com/api/users'
context . url . searchParams . get ( 'id' )
Use Cases
Request Logging
Log all requests for monitoring:
export const onRequest = defineMiddleware (( context , next ) => {
const start = Date . now ()
console . log ( `[ ${ context . request . method } ] ${ context . url . pathname } ` )
const response = await next ()
const duration = Date . now () - start
console . log ( `Completed in ${ duration } ms` )
return response
})
Authentication
Protect routes with authentication checks:
export const onRequest = defineMiddleware ( async ( context , next ) => {
const protectedPaths = [ '/dashboard' , '/admin' , '/api/private' ]
const isProtected = protectedPaths . some ( path =>
context . url . pathname . startsWith ( path )
)
if ( isProtected ) {
const token = context . cookies . get ( 'session' )?. value
if ( ! token ) {
return new Response ( 'Unauthorized' , { status: 401 })
}
try {
const user = await verifyToken ( token )
context . locals . user = user
} catch ( error ) {
return new Response ( 'Invalid token' , { status: 401 })
}
}
return next ()
})
Redirects
Implement custom redirect logic:
export const onRequest = defineMiddleware (( context , next ) => {
// Redirect old URLs to new ones
if ( context . url . pathname === '/old-path' ) {
return Response . redirect ( new URL ( '/new-path' , context . url ), 301 )
}
// Force HTTPS
if ( context . url . protocol === 'http:' ) {
const httpsUrl = context . url . href . replace ( 'http:' , 'https:' )
return Response . redirect ( httpsUrl , 301 )
}
return next ()
})
Request Modification
Add headers or modify requests:
export const onRequest = defineMiddleware ( async ( context , next ) => {
// Add custom headers to the request
context . request . headers . set ( 'x-custom-header' , 'value' )
// Add request ID for tracing
const requestId = crypto . randomUUID ()
context . locals . requestId = requestId
const response = await next ()
// Add headers to the response
response . headers . set ( 'x-request-id' , requestId )
response . headers . set ( 'x-powered-by' , 'Astro + Vercel' )
return response
})
Geolocation
Access user location information:
export const onRequest = defineMiddleware (( context , next ) => {
const country = context . request . headers . get ( 'x-vercel-ip-country' )
const city = context . request . headers . get ( 'x-vercel-ip-city' )
const region = context . request . headers . get ( 'x-vercel-ip-country-region' )
// Store geolocation in locals
context . locals . geo = {
country ,
city: decodeURIComponent ( city || '' ),
region ,
}
// Redirect based on location
if ( country === 'US' && context . url . pathname === '/' ) {
return Response . redirect ( new URL ( '/us' , context . url ), 302 )
}
return next ()
})
Geolocation headers are only available on Vercel’s Edge Network. They won’t be present in local development.
Middleware Execution Flow
Request Arrives : Middleware intercepts the request
Pre-Processing : Middleware logic runs before next()
Route Handler : Page or API endpoint executes
Post-Processing : Middleware can modify the response
Response Sent : Final response returned to client
export const onRequest = defineMiddleware ( async ( context , next ) => {
// 1. Pre-processing
console . log ( 'Before request' )
// 2. Execute route handler
const response = await next ()
// 3. Post-processing
console . log ( 'After request' )
// 4. Return response
return response
})
Multiple Middleware Functions
Chain multiple middleware functions:
import { sequence } from 'astro:middleware'
const logger = defineMiddleware ( async ( context , next ) => {
console . log ( `[ ${ context . request . method } ] ${ context . url . pathname } ` )
return next ()
})
const auth = defineMiddleware ( async ( context , next ) => {
const token = context . cookies . get ( 'session' )?. value
if ( token ) {
context . locals . user = await verifyToken ( token )
}
return next ()
})
const cors = defineMiddleware ( async ( context , next ) => {
const response = await next ()
response . headers . set ( 'Access-Control-Allow-Origin' , '*' )
return response
})
// Chain middleware functions
export const onRequest = sequence ( logger , auth , cors )
Accessing Middleware Data
Access data from middleware in your pages:
---
// Access data set in middleware
const user = Astro . locals . user
const requestId = Astro . locals . requestId
const geo = Astro . locals . geo
---
< Layout >
{ user ? (
< p > Welcome, { user . name } ! </ p >
) : (
< p > Please log in </ p >
) }
< p > Request ID: { requestId } </ p >
< p > Location: { geo . city } , { geo . country } </ p >
</ Layout >
The boilerplate includes a /locals demo page (src/pages/locals.astro) that shows how to access middleware data using Astro.locals.
Keep It Light
Conditional Execution
Edge Benefits
Middleware runs on every request, so keep it fast: // Good: Simple checks
if ( context . url . pathname === '/protected' ) {
// Quick auth check
}
// Avoid: Heavy computations
// await heavyDatabaseQuery()
Only run logic when needed: const needsAuth = context . url . pathname . startsWith ( '/api' )
if ( needsAuth ) {
// Only authenticate API routes
}
Edge Middleware provides:
Global distribution
Low latency
Auto-scaling
Fast cold starts
Best Practices
Keep middleware fast : Avoid heavy operations
Use locals for data : Store request-scoped data in context.locals
Handle errors gracefully : Always return a response
Log strategically : Don’t log sensitive data
Test thoroughly : Middleware affects all requests
Use middleware for cross-cutting concerns that apply to multiple routes, not route-specific logic.
Next Steps
SSR Use middleware with SSR pages
Edge Functions Combine with Edge Functions