Skip to main content
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:
src/middleware.ts
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:
src/middleware.ts
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

request
Request
The incoming HTTP request
url
URL
Parsed URL object
params
Record<string, string>
Route parameters from dynamic routes
cookies
AstroCookies
Cookie utilities for reading/writing cookies
locals
App.Locals
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
clientAddress
string
Client’s IP address

Authentication Example

Implement authentication in middleware:
src/middleware.ts
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:
src/middleware.ts
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:
src/middleware/logger.ts
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;
});
src/middleware/cors.ts
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:
  1. logger runs first
  2. cors runs second
  3. auth runs last
  4. Page handler executes
  5. 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:
src/middleware.ts
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:
src/middleware.ts
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:
src/middleware.ts
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.
Work with cookies in middleware:
src/middleware.ts
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();
});
path
string
Cookie path. Default: ’/’
domain
string
Cookie domain
expires
Date
Expiration date
maxAge
number
Max age in seconds
httpOnly
boolean
HTTP-only flag (not accessible via JavaScript)
secure
boolean
Secure flag (HTTPS only)
sameSite
'strict' | 'lax' | 'none'
SameSite attribute

Type Safety

Type your locals object for better TypeScript support:
src/env.d.ts
/// <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:
src/middleware.ts
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:
src/pages/api/data.ts
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

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

Conditional Middleware

Run middleware conditionally based on the route:
src/middleware.ts
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

SSR

Server-side rendering

API Routes

Build API endpoints

Authentication

Authentication patterns

Build docs developers (and LLMs) love