astro-middleware

How to Use Middleware in Astro

  • 5 min

Middleware is code that runs before Astro processes any page or endpoint. It allows us to intercept the request, modify it, or block it completely.

If you’ve worked with backend frameworks like Express or Koa, this concept will be familiar.

Unlike other frameworks where middleware only exists if you have a Node.js server running, in Astro middleware is available in both modes, although its execution timing changes:

  • In SSR mode (output: ‘server’): Middleware runs at request time. Every time a user visits a page, the middleware activates. It’s ideal for authentication, reading cookies, or dynamic redirects.

  • In SSG mode (output: ‘static’): Middleware runs at build time. It activates when you run npm run build while Astro generates the HTML pages. It’s useful for manipulating the final HTML, injecting data at build time, or generating build logs.

In this article, we’ll mainly focus on dynamic use cases (SSR), which is where middleware makes the most sense (route protection, users), but keep in mind the architecture is the same for both.

Creating the Middleware

In Astro, middleware is defined in a single special file: src/middleware.ts (or .js). You cannot give it another name or move it to another folder.

The basic structure is as follows:

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

// The 'onRequest' function intercepts ALL requests
export const onRequest = defineMiddleware(async (context, next) => {
  console.log("Intercepted request to:", context.url.pathname);

  // 'next()' passes control to the next step (render the page)
  // We must return its response
  const response = await next();

  console.log("Page rendered. Returning response...");
  return response;
});
Copied!

This code acts like a “sandwich”. We do things before next() (pre-processing) and things after next() (post-processing).

Chaining Middleware (sequence)

In large applications, having all the code in a single onRequest function can be chaotic. You might have authentication logic, translation logic (i18n), analytics logic, etc.

Astro allows us to split the middleware into small functions and chain them using sequence.

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

async function auth(context, next) {
  console.log("1. Checking Auth");
  // Auth logic...
  return next();
}

async function validation(context, next) {
  console.log("2. Validating data");
  // Validation logic...
  return next();
}

async function logging(context, next) {
  console.log("3. Logging visit");
  return next();
}

// Export the sequence in order
export const onRequest = sequence(auth, validation, logging);
Copied!

Order matters: auth will run first. If auth decides to redirect and doesn’t call next(), validation and logging will never run.

Response Manipulation

Sometimes we want to modify the HTML response after the page has been rendered. For example, to add security headers or minify the HTML.

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next(); // Wait for the HTML to be generated

  // Modify headers
  response.headers.set("X-Powered-By", "Astro + LuisLlamas");

  // We can even modify the HTML (be careful with performance)
  const html = await response.text();
  const newHtml = html.replace("</body>", "</body>");

  return new Response(newHtml, {
    status: 200,
    headers: response.headers
  });
});
Copied!

Intercepting and rewriting the HTML body (response.text()) has a high performance cost. Use it only if strictly necessary.