Skip to content

Astro middleware #531

@matthewp

Description

@matthewp

Body

Summary

Introduce a middleware to Astro, where you can define code that runs on every request. This API should work regardless of the rendering mode (SSG or SSR) or adapter used. Also introduces a simple way to share request-specific data between proposed middleware, API routes, and .astro routes.

Background & Motivation

Middleware has been one of the most heavily requested feature in Astro. It's useful for handling common tasks like auth guards and setting cache headers. For me, it would make handling authentication much easier.

This proposal is heavily inspired by SvelteKit's handle hooks;

Goals

  • Provide a way intercept requests and responses, allowing users to set cookies and headers
  • Works both in SSR and SSG mode
  • Allow users to use community-created middlewares (libraries)
    • Make available via integrations API.
  • Provide an API for request-specific data
  • Non-Node runtimes specific APIs. ie. Cloudflare Durable Objects.
    • Add middleware from adapter.

Non-Goals

  • Route specific middleware, middlewares that are run only on specific routes

Example

A quick prototype

Middlewares can be defined in src/middleware.ts by exporting middleware (array):

export cost middleware = []

A simple middleware looks like so:

export const middleware: Middleware = async (context: APIContext, resolve: Resolve) => {
  const session = await getSession(context.request);
  const isProtectedRoute = matchRoute(context.url);
  if (!session && isProtectedRoute) {
    // early response
    return new Response(null, {
      status: 400,
    });
  }
  context.cookies.set("random-cookie", "some-value");
  const response = await resolve(context); // resolve api route or render page
  return response;
};

context is the same one provided to API route handlers. Most of Astro's request handling process will be behind resolve(), and the response object returned from middleware will be sent to the user's browser.

Multiple middleware

sequence can be imported to run multiple middlewares in sequence.

export const middleware: Middleware = sequence(
  async (context, resolve) => {
    console.log("1a");
    const response = await resolve(event);
    console.log("1b");
    return response;
  },
  async (context, resolve) => {
    console.log("2a");
    const response = await resolve(event);
    console.log("2b");
    return response;
  }
);

The log result for this example is:

1a
2a
2b
1b

locals

This proposal also adds locals property to APIContext and AsroGlobal. This locals object will be forwarded across the request handling process, allowing for 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.

export const middleware = async (context: APIContext, resolve: Resolve) => {
  context.session = await getSession(context.request);
  const response = await resolve(context);
  return response;
};
---
// pages/index.astro
const session = Astro.locals.session;
---

The value type of locals can be anything as it won't be JSON-stringified:

---
// pages/index.astro
const session = await Astro.locals.getSession();
---

locals can be typed inside src/env.d.ts (I think it's possible?):

// src/env.d.ts
// at least one of these should be possible
type Locals {}

declare namespace App {
    type Locals = {}
}

SSG mode

The middleware function will run on each route's pre-rendering step, as if it were handling a normal SSR request.

export const middleware = async (context: APIContext, resolve: Resolve) => {
  const response = await resolve(context); // pre-render step here
  return response;
};

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Implemented

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions