Middleware

Astro middleware intercepts requests and responses, runs before page rendering, and can share data across the request lifecycle via locals.

Basic middleware

Create src/middleware.ts:

import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware((context, next) => {
  // Runs before every page/endpoint
  console.log(`Request: ${context.url.pathname}`);
  return next();
});

Modifying the response

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next();

  // Add headers to every response
  response.headers.set('X-Custom-Header', 'value');

  return response;
});

Redirects

export const onRequest = defineMiddleware((context, next) => {
  if (context.url.pathname === '/daily/today') {
    const today = new Date().toISOString().split('T')[0];
    return context.redirect(`/daily/${today}`);
  }
  return next();
});

Sharing data with locals

Middleware can attach data to context.locals, accessible in pages and endpoints:

// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  const token = context.cookies.get('session')?.value;
  if (token) {
    context.locals.user = await verifyToken(token);
  }
  return next();
});
---
// src/pages/dashboard.astro
const user = Astro.locals.user;
if (!user) {
  return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>

Type locals in src/env.d.ts:

/// <reference types="astro/client" />
declare namespace App {
  interface Locals {
    user?: { id: string; name: string; };
  }
}

Chaining middleware

Use sequence() to compose multiple middleware:

import { defineMiddleware, sequence } from 'astro:middleware';

const auth = defineMiddleware(async (context, next) => {
  // auth logic
  return next();
});

const logging = defineMiddleware(async (context, next) => {
  const start = Date.now();
  const response = await next();
  console.log(`${context.url.pathname} - ${Date.now() - start}ms`);
  return response;
});

export const onRequest = sequence(auth, logging);

Static vs on-demand behavior

  • Static pages: middleware runs at build time only
  • On-demand pages: middleware runs per request

If you need middleware to run per request (auth, redirects, logging), the relevant routes must be on-demand (prerender = false).

Comparison with Next.js middleware

FeatureNext.jsAstro
Locationmiddleware.ts at project rootsrc/middleware.ts
Runs onEdge runtime (limited APIs)Same runtime as pages
Data sharingHeaders/cookiescontext.locals (typed)
ComposableSingle middleware functionsequence() for chaining
Static pagesRuns per request (edge)Runs at build time only
Matcherconfig.matcher patternCheck context.url manually

Key advantage of Astro middleware: it runs in the same Node.js runtime as your pages, so you have full access to Node APIs, databases, and filesystem. Next.js middleware runs on the Edge runtime with significant limitations.