Skip to content
Astro as patterns 4 min read

Middleware

Astro middleware lets you run code on every request before a page or endpoint renders. It is the right place for cross-cutting concerns — authentication checks, redirects, security headers, locale detection, and injecting shared data into locals. Because middleware runs on the server, it works in SSR and on-demand routes, keeping your pages and components clean while centralizing logic that would otherwise be copy-pasted everywhere.

The onRequest hook

Middleware lives in a single file, src/middleware.ts (or src/middleware/index.ts), which exports an onRequest function. Astro calls it for each incoming request, passing a context object and a next function. Calling next() continues the chain and returns the eventual Response; you can return your own Response instead to short-circuit rendering entirely.

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  console.log(`[req] ${context.request.method} ${context.url.pathname}`);

  // Continue to the matched route and get its Response
  const response = await next();

  // Mutate the outgoing response
  response.headers.set("X-Powered-By", "Astro");
  return response;
});

Output:

[req] GET /
[req] GET /blog/hello-world

defineMiddleware is a no-op helper that exists purely for type inference — it gives you a fully typed context and next. You can also export a plain async function, but the helper is the idiomatic choice.

The context object

The context argument is the same APIContext you get inside .astro pages and endpoints. The most useful members:

PropertyDescription
context.requestThe standard Request object (method, headers, body).
context.urlA URL instance for the current request.
context.cookiesRead and write cookies via get, set, delete.
context.localsA mutable object shared with the rendered page/endpoint.
context.redirect()Returns a redirect Response.
context.rewrite()Renders a different route without changing the URL.
context.params / context.propsRoute params and props for the matched route.

Sharing data with locals

context.locals is the canonical way to pass server-computed data from middleware into pages without prop drilling or refetching. Anything you assign survives for the lifetime of that single request.

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const token = context.cookies.get("session")?.value;
  context.locals.user = token ? await getUserFromToken(token) : null;
  return next();
});

async function getUserFromToken(token: string) {
  // Real lookup against your auth provider / database
  const res = await fetch("https://api.example.com/me", {
    headers: { Authorization: `Bearer ${token}` },
  });
  return res.ok ? await res.json() : null;
}

Read it back in any page or endpoint:

---
// src/pages/dashboard.astro
const { user } = Astro.locals;
if (!user) return Astro.redirect("/login");
---
<h1>Welcome back, {user.name}</h1>

To get autocompletion and type safety on locals, declare its shape in src/env.d.ts:

// src/env.d.ts
declare namespace App {
  interface Locals {
    user: { id: string; name: string } | null;
  }
}

Tip: locals must be a plain serializable object on prerendered pages. For SSR routes you can store richer values, but avoid stuffing large blobs — it lives in memory for the whole request.

Guarding routes and redirecting

Returning a Response from middleware stops the request before the page renders. Combine this with context.url to protect entire route prefixes in one place.

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

const PROTECTED = ["/dashboard", "/account"];

export const onRequest = defineMiddleware(async (context, next) => {
  const path = context.url.pathname;
  const needsAuth = PROTECTED.some((p) => path.startsWith(p));

  if (needsAuth && !context.cookies.has("session")) {
    return context.redirect("/login?next=" + encodeURIComponent(path));
  }

  return next();
});

Rewrites

context.rewrite() renders a different route while keeping the URL in the address bar unchanged — ideal for maintenance pages, A/B tests, or serving a localized variant.

export const onRequest = defineMiddleware((context, next) => {
  if (context.url.pathname === "/old-pricing") {
    return context.rewrite("/pricing"); // URL stays /old-pricing
  }
  return next();
});

Chaining multiple middleware

Larger apps split concerns into separate functions and compose them with the sequence helper. They run in array order; each next() flows to the following one, then back up.

// src/middleware.ts
import { sequence, defineMiddleware } from "astro:middleware";

const auth = defineMiddleware(async (context, next) => {
  context.locals.user = await resolveUser(context);
  return next();
});

const headers = defineMiddleware(async (_context, next) => {
  const response = await next();
  response.headers.set("X-Frame-Options", "DENY");
  return response;
});

export const onRequest = sequence(auth, headers);

async function resolveUser(_context: unknown) {
  return null;
}

Because next() returns the downstream Response, middleware earlier in the sequence wraps the work of later ones — letting you both prepare locals on the way in and decorate the response on the way out.

Best practices

  • Keep onRequest fast — it runs on every request, so push heavy work behind caching or conditionals on context.url.pathname.
  • Type App.Locals in src/env.d.ts so pages get autocompletion instead of any.
  • Return early with context.redirect() for auth gates rather than rendering and hiding content.
  • Split unrelated logic into separate functions composed with sequence for readability.
  • Use context.rewrite() (not a 3xx redirect) when the URL should stay put.
  • Set security headers (CSP, X-Frame-Options) on the response in one central middleware.
  • Remember middleware only runs for on-demand/SSR routes; fully prerendered pages skip it at request time.
Last updated June 14, 2026
Was this helpful?