Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/withastro/astro/llms.txt

Use this file to discover all available pages before exploring further.

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: 'john@example.com',
    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