AvalonAvalon
GitHub

Middleware

Run server-side logic before pages render — auth checks, redirects, request logging, and more.

Middleware runs on the server before a page renders. Use it for authentication, redirects, logging, or injecting data into the request context. Avalon supports two kinds: global middleware that runs on every request, and scoped middleware that targets specific routes.

Global middleware

Global middleware runs on every incoming request. Place files in the middleware/ directory at your project root:

middleware/
  logging.ts       ← runs on every request
  cors.ts          ← runs on every request

These are standard Nitro middleware files. Use defineEventHandler from h3:

// middleware/logging.ts
import { defineEventHandler } from 'h3';

export default defineEventHandler((event) => {
  console.log(`${event.method} ${event.path}`);
  // Return nothing to continue to the next handler
});

Nitro auto-discovers files in middleware/ — no configuration needed. They run in alphabetical order, before any page or API route handler.

CORS example

// middleware/cors.ts
import { defineEventHandler, setResponseHeaders } from 'h3';

export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
  });
});

Scoped middleware

Scoped middleware runs only for routes within a specific directory. Create a _middleware.ts file in any pages directory:

app/modules/
  admin/
    pages/
      _middleware.ts    ← runs for /admin/*
      index.tsx
      settings.tsx
  blog/
    pages/
      _middleware.ts    ← runs for /blog/*
      index.tsx
      [slug].tsx

Export a default function that receives an H3 event. Return nothing to continue, or return a Response to stop the chain:

// app/modules/admin/pages/_middleware.ts
import type { H3Event } from 'h3';

export default async (event: H3Event) => {
  const token = event.req.headers.get('Authorization');

  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }

  event.context.user = await validateToken(token);
};

Execution order

Global middleware runs first (alphabetical), then scoped middleware runs parent-first by directory depth:

middleware/logging.ts                              ← 1st (global)
middleware/cors.ts                                 ← 2nd (global, alphabetical)
app/modules/admin/pages/_middleware.ts             ← 3rd (scoped, depth 0)
app/modules/admin/pages/settings/_middleware.ts    ← 4th (scoped, depth 1)

Passing data to pages

Use event.context to pass data from middleware to downstream handlers:

// _middleware.ts
export default async (event: H3Event) => {
  event.context.locale = detectLocale(event.req.headers.get('Accept-Language'));
};

Redirects

Return a Response with a redirect status:

export default async (event: H3Event) => {
  const url = new URL(event.req.url);

  if (url.pathname === '/old-page') {
    return Response.redirect(new URL('/new-page', url.origin), 301);
  }
};

Global vs scoped — when to use which

Global (middleware/)Scoped (_middleware.ts)
Runs onEvery requestMatching routes only
Applies toPages + API routesPages only
LocationProject root middleware/Inside module pages/ dirs
Use forLogging, CORS, security headersAuth guards, locale detection, redirects

Using other server frameworks

Avalon uses Nitro as its server layer, and Nitro supports plugging in other web frameworks as the router. If you prefer Hono, Elysia, or another framework over h3, you can use them for your API routes and middleware.

Create a routes/_app.ts file to mount a framework as the route handler:

// routes/_app.ts — using Hono
import { Hono } from 'hono';
import { cors } from 'hono/cors';

const app = new Hono();

app.use('/*', cors());

app.get('/api/hello', (c) => c.json({ message: 'Hello from Hono' }));

export default app;
// routes/_app.ts — using Elysia
import { Elysia } from 'elysia';

const app = new Elysia()
  .get('/api/hello', () => ({ message: 'Hello from Elysia' }));

export default app;

This works because Nitro v3 supports any framework that exports a Web API-compatible handler. The scoped _middleware.ts files still use h3 events since they're part of Avalon's page rendering pipeline.

Performance

Scoped middleware is cached in production and reloaded via Vite in development so changes apply immediately. If any middleware takes longer than 100ms, Avalon logs a warning. The default timeout is 30 seconds.