Skip to main content

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:
astro.config.mjs
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:
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:
src/middleware.ts
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:
src/middleware.ts
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:
src/middleware.ts
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:
src/middleware.ts
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:
src/middleware.ts
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

  1. Request Arrives: Middleware intercepts the request
  2. Pre-Processing: Middleware logic runs before next()
  3. Route Handler: Page or API endpoint executes
  4. Post-Processing: Middleware can modify the response
  5. 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:
src/middleware.ts
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:
src/pages/index.astro
---
// 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.

Performance Considerations

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()

Best Practices

  1. Keep middleware fast: Avoid heavy operations
  2. Use locals for data: Store request-scoped data in context.locals
  3. Handle errors gracefully: Always return a response
  4. Log strategically: Don’t log sensitive data
  5. 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

Build docs developers (and LLMs) love