Middleware

Middleware are functions that run during the request lifecycle, before your route handler executes. They're perfect for tasks like logging, authentication, adding headers, or validating requests.

Quick Start

import { MageApp } from "@mage/app";

const app = new MageApp();

// Simple logging middleware
app.use(async (c, next) => {
  console.log(`${c.req.method} ${c.req.url.pathname}`);
  await next();
});

app.get("/", (c) => c.text("Hello!"));

Deno.serve(app.handler);

How Middleware Works

Middleware functions receive two parameters:

import type { MageMiddleware } from "@mage/app";

const myMiddleware: MageMiddleware = async (c, next) => {
  // c: MageContext - the request/response context
  // next: function - calls the next middleware in the chain

  // Code here runs BEFORE the handler
  console.log("Before");

  await next();

  // Code here runs AFTER the handler
  console.log("After");
};

Key concepts:

  • Calling next() continues to the next middleware or handler
  • Not calling next() stops the chain and returns the current response
  • Middleware can modify the context before and after calling next()
  • Middleware can be synchronous or asynchronous

Middleware Signature

The middleware type signature is:

type MageMiddleware = (
  context: MageContext,
  next: () => Promise<void> | void,
) => Promise<void> | void;

Both the middleware function and next() can return either void or Promise<void>, allowing for both synchronous and asynchronous middleware.

Execution Order

Middleware executes in a specific order:

  1. Global middleware (registered with app.use())
  2. Route-specific middleware (registered with route methods)
  3. Route handler (the final function)
import { MageApp } from "@mage/app";

const app = new MageApp();

// 1. Global middleware - runs first
app.use(async (c, next) => {
  console.log("Global middleware");
  await next();
});

// 2. Route middleware - runs second
// 3. Handler - runs last
app.get(
  "/users",
  async (c, next) => {
    console.log("Route middleware");
    await next();
  },
  (c) => {
    console.log("Handler");
    return c.json({ users: [] });
  },
);

// Output on GET /users:
// Global middleware
// Route middleware
// Handler

Within each category (global, route-specific), middleware executes in registration order.

Global Middleware

Global middleware runs for every request, regardless of the route or HTTP method.

import { MageApp } from "@mage/app";
import { cors } from "@mage/app/cors";
import { requestId } from "@mage/app/request-id";

const app = new MageApp();

// These run for ALL requests
app.use(cors({ origins: "*" }));
app.use(requestId());
app.use(async (c, next) => {
  console.log(`${c.req.method} ${c.req.url.pathname}`);
  await next();
});

app.get("/", (c) => c.text("Hello!"));
app.post("/users", (c) => c.json({ created: true }));

Route-Specific Middleware

Apply middleware to specific routes by passing them to route methods:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

const requireAuth = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  if (token !== "secret-token") {
    throw new MageError("Unauthorized", 401);
  }
  await next();
};

// Middleware only runs for this route
app.get("/admin", requireAuth, (c) => {
  return c.json({ admin: true });
});

// Public route - no auth middleware
app.get("/", (c) => c.text("Hello!"));

You can chain multiple middleware on a single route:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

const requireAuth = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  if (token !== "secret-token") {
    throw new MageError("Unauthorized", 401);
  }
  await next();
};

const validateBody = async (c, next) => {
  const body = await c.req.json();

  if (!body.name) {
    throw new MageError("Name is required", 400);
  }

  c.set("validatedBody", body);
  await next();
};

// Auth runs first, then validation, then handler
app.post("/users", requireAuth, validateBody, (c) => {
  const body = c.get("validatedBody");
  return c.json({ id: 1, ...body }, 201);
});

Cross-Method Middleware

Use app.all() to apply middleware to all HTTP methods for a route:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

const requireAuth = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  if (token !== "secret-token") {
    throw new MageError("Unauthorized", 401);
  }
  await next();
};

// Applies to GET, POST, PUT, DELETE, etc. on /admin
app.all("/admin", requireAuth);

app.get("/admin", (c) => c.json({ data: "admin data" }));
app.post("/admin", (c) => c.json({ updated: true }));
app.delete("/admin", (c) => c.empty(204));

Route Grouping

Apply middleware to multiple routes using wildcard patterns:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

const requireAuth = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  if (token !== "secret-token") {
    throw new MageError("Unauthorized", 401);
  }
  await next();
};

// Apply auth to all routes starting with /admin
app.all("/admin/*", requireAuth);

app.get("/admin/users", (c) => c.json({ users: [] }));
app.get("/admin/settings", (c) => c.json({ settings: {} }));
app.post("/admin/users", (c) => c.json({ created: true }));

// Public routes - no auth required
app.get("/", (c) => c.text("Hello!"));
app.get("/about", (c) => c.text("About"));

Middleware registered with patterns executes according to route specificity:

  • Static routes match first (/admin/users)
  • Parameterized routes match next (/admin/:section)
  • Wildcard routes match last (/admin/*)

Middleware Composition

You can pass multiple middleware at once using arrays:

import { MageApp } from "@mage/app";

const app = new MageApp();

const logRequest = async (c, next) => {
  console.log(`${c.req.method} ${c.req.url.pathname}`);
  await next();
};

const addTimestamp = async (c, next) => {
  c.set("timestamp", Date.now());
  await next();
};

const addHeader = async (c, next) => {
  c.header("X-Custom", "value");
  await next();
};

// All three run in order
app.use([logRequest, addTimestamp, addHeader]);

// Or mix arrays and individual middleware
app.use(logRequest, [addTimestamp, addHeader]);

app.get("/", (c) => c.text("Hello!"));

Creating reusable middleware groups:

import { MageApp } from "@mage/app";
import { cors } from "@mage/app/cors";
import { requestId } from "@mage/app/request-id";

const app = new MageApp();

// Define a group of common middleware
const commonMiddleware = [
  cors({ origins: "*" }),
  requestId(),
  async (c, next) => {
    console.log(`${c.req.method} ${c.req.url.pathname}`);
    await next();
  },
];

// Apply the group
app.use(commonMiddleware);

app.get("/", (c) => c.text("Hello!"));

Context Sharing

Middleware can share data using c.set() and c.get():

import { MageApp } from "@mage/app";

const app = new MageApp();

// Middleware sets data
app.use(async (c, next) => {
  const startTime = Date.now();
  c.set("startTime", startTime);

  await next();

  const duration = Date.now() - startTime;
  console.log(`Request took ${duration}ms`);
});

// Handler reads data
app.get("/", (c) => {
  const startTime = c.get<number>("startTime");
  return c.json({ startTime });
});

TypeScript tip: Use generics with c.get() for type safety:

interface User {
  id: number;
  name: string;
}

app.use(async (c, next) => {
  const user = { id: 1, name: "Alice" };
  c.set("user", user);
  await next();
});

app.get("/profile", (c) => {
  // TypeScript knows user is of type User
  const user = c.get<User>("user");
  return c.json(user);
});

Early Returns

Stop the middleware chain by not calling next():

import { MageApp } from "@mage/app";

const app = new MageApp();

const checkMaintenance = async (c, next) => {
  const isMaintenanceMode = true;

  if (isMaintenanceMode) {
    // Don't call next() - return early
    return c.text("Service unavailable", 503);
  }

  await next();
};

app.use(checkMaintenance);

// This handler won't run during maintenance
app.get("/", (c) => c.text("Hello!"));

Another example with authentication:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

const requireAuth = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");

  if (!token) {
    // Stop here - don't call next()
    throw new MageError("Unauthorized", 401);
  }

  const user = await validateToken(token);
  c.set("user", user);
  await next();
};

app.get("/protected", requireAuth, (c) => {
  return c.json({ message: "You're authenticated!" });
});

Writing Custom Middleware

Middleware can be a simple function or a factory function that returns middleware:

Simple Middleware

import type { MageMiddleware } from "@mage/app";

const logRequest: MageMiddleware = async (c, next) => {
  console.log(`${c.req.method} ${c.req.url.pathname}`);
  await next();
};

app.use(logRequest);

Factory Function

Create configurable middleware using factory functions:

import type { MageMiddleware } from "@mage/app";

interface LogOptions {
  includeTimestamp?: boolean;
  includeHeaders?: boolean;
}

const logger = (options?: LogOptions): MageMiddleware => {
  return async (c, next) => {
    const parts: string[] = [];

    if (options?.includeTimestamp) {
      parts.push(new Date().toISOString());
    }

    parts.push(`${c.req.method} ${c.req.url.pathname}`);

    if (options?.includeHeaders) {
      parts.push(JSON.stringify(Object.fromEntries(c.req.headers)));
    }

    console.log(parts.join(" - "));
    await next();
  };
};

// Use with options
app.use(
  logger({
    includeTimestamp: true,
    includeHeaders: false,
  }),
);

Middleware with Before/After Logic

Run code before and after the handler:

import type { MageMiddleware } from "@mage/app";

const timer: MageMiddleware = async (c, next) => {
  const start = Date.now();

  await next();

  const duration = Date.now() - start;
  c.header("X-Response-Time", `${duration}ms`);
};

app.use(timer);

Async Middleware

Middleware can be asynchronous to handle async operations:

import { MageApp } from "@mage/app";

const app = new MageApp();

const loadUser = async (c, next) => {
  const userId = c.req.header("X-User-ID");

  if (userId) {
    // Async database call
    const user = await db.users.findById(userId);
    c.set("user", user);
  }

  await next();
};

app.use(loadUser);

app.get("/profile", (c) => {
  const user = c.get("user");
  return c.json(user ?? { guest: true });
});

Always await next() when using async middleware to ensure proper error handling and execution order.

Error Handling

Middleware can throw errors using MageError or catch errors from other middleware using try-catch:

import { MageApp, MageError } from "@mage/app";

const app = new MageApp();

// Throwing errors in middleware
const validateApiKey = async (c, next) => {
  const apiKey = c.req.header("X-API-Key");

  if (!apiKey) {
    throw new MageError("API key required", 401);
  }

  await next();
};

// Catching errors in middleware
const errorHandler = async (c, next) => {
  try {
    await next();
  } catch (error) {
    if (error instanceof MageError) {
      return c.text(error.message, error.status);
    }
    return c.text("Internal Server Error", 500);
  }
};

app.use(errorHandler);
app.use(validateApiKey);

For comprehensive error handling patterns, see Error Handling.

Important Notes

Don't Call next() Multiple Times

Calling next() more than once in the same middleware will throw an error:

const bad = async (c, next) => {
  await next();
  await next(); // Error: next() called multiple times
};

This prevents bugs where middleware accidentally continues the chain twice.

Order Matters

Middleware executes in registration order. Register middleware in the order you want it to run:

import { MageApp } from "@mage/app";

const app = new MageApp();

// CORS must run before other middleware to handle preflight
app.use(cors({ origins: "*" }));

// Auth runs after CORS
app.use(requireAuth);

// Logger runs last
app.use(logRequest);

Global vs Route-Specific

Choose the appropriate scope for your middleware:

  • Use global middleware (app.use()) for cross-cutting concerns: logging, CORS, request IDs, error handling
  • Use route-specific middleware for features tied to specific routes: authentication, validation, rate limiting
  • MageApp - Registering middleware with the app
  • MageContext - The context object passed to middleware
  • Error Handling - Handling errors in middleware
  • Routing - Route patterns and wildcards for middleware