Skip to content

Middleware

Middleware allows you to intercept requests and responses and inject behaviors dynamically every time a page or endpoint is about to be rendered.

This also allows you to set and share request-specific information across endpoints and pages by mutating a locals object that is available in all Astro components and API endpoints

Middleware is available in both SSG and SSR Astro projects.

  1. Create src/middleware.js|ts (Alternatively, you can create src/middleware/index.js|ts.)

  2. Inside this file, export an onRequest() function that can be passed a context object and next() function. This must not be a default export.

    src/middleware.js
    export function onRequest ({ locals, request }, next) {
    // intercept data from a request
    // optionally, modify the properties in `locals`
    locals.title = "New title";
    // return a Response or the result of calling `next()`
    return next();
    };
  3. Inside any .astro file, access response data using Astro.locals.

    src/components/Component.astro
    ---
    const data = Astro.locals;
    ---
    <h1>{data.title}</h1>
    <p>This {data.property} is from middleware.</p>

The context object includes information to be made available to other middleware, API routes and .astro routes during the rendering process.

This is an optional argument passed to onRequest() that may contain the locals object as well as any additional properties to be shared during rendering. For example, the context object may include cookies used in authentication.

Storing data in context.locals

Section titled Storing data in context.locals

context.locals is an object that can be manipulated inside the middleware.

This locals object is forwarded across the request handling process and is available as a property to APIContext and AstroGlobal. This allows data to be shared between middlewares, API routes, and .astro pages. This is useful for storing request-specific data, such as user data, across the rendering step.

You can store any type of data inside locals: strings, numbers, and even complex data types such as functions and maps.

src/middleware.js
export function onRequest ({ locals, request }, next) {
// intercept data from a request
// optionally, modify the properties in `locals`
locals.user.name = "John Wick";
locals.welcomeTitle = () => {
return "Welcome back " + locals.user.name;
};
// return a Response or the result of calling `next()`
return next();
};

Then you can use this information inside any .astro file with Astro.locals.

src/pages/orders.astro
---
const title = Astro.locals.welcomeTitle();
const orders = Array.from(Astro.locals.orders.entries());
---
<h1>{title}</h1>
<p>This {data.property} is from middleware.</p>
<ul>
{orders.map(order => {
return <li>{/* do something with each order */}</li>;
})}
</ul>

locals is an object that lives and dies within a single Astro route; when your route page is rendered, locals won’t exist anymore and a new one will be created. Information that needs to persist across multiple page requests must be stored elsewhere.

Example: redacting sensitive information

Section titled Example: redacting sensitive information

The example below uses middleware to replace “PRIVATE INFO” with the word “REDACTED” to allow you to render modified HTML on your page:

src/middleware.js
export const onRequest = async (context, next) => {
const response = await next();
const html = await response.text();
const redactedHtml = html.replaceAll("PRIVATE INFO", "REDACTED");
return new Response(redactedHtml, {
status: 200,
headers: response.headers
});
};

You can import and use the utility function defineMiddleware() to take advantage of type safety:

src/middleware.ts
import { defineMiddleware } from "astro:middleware";
// `context` and `next` are automatically typed
export const onRequest = defineMiddleware((context, next) => {
});

Instead, if you’re using JsDoc to take advantage of type safety, you can use MiddlewareHandler:

src/middleware.js
/**
* @type {import("astro").MiddlewareHandler}
*/
// `context` and `next` are automatically typed
export const onRequest = (context, next) => {
};

To type the information inside Astro.locals, which gives you autocompletion inside .astro files and middleware code, declare a global namespace in the env.d.ts file:

src/env.d.ts
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
user: {
name: string
},
welcomeTitle: () => string,
orders: Map<string, object>
}
}

Then, inside the middleware file, you can take advantage of autocompletion and type safety.

Multiple middlewares can be joined in a specified order using sequence():

src/middleware.js
import { sequence } from "astro:middleware";
async function validation(_, next) {
console.log("validation request");
const response = await next();
console.log("validation response");
return response;
}
async function auth(_, next) {
console.log("auth request");
const response = await next();
console.log("auth response");
return response;
}
async function greeting(_, next) {
console.log("greeting request");
const response = await next();
console.log("greeting response");
return response;
}
export const onRequest = sequence(validation, auth, greeting);

This will result in the following console order:

Terminal window
validation request
auth request
greeting request
greeting response
auth response
validation response