camelon

Middleware

A directory can hold a _middleware.ts. It applies to that directory and everything under it. Pipelines run outermost first, then the route handler. Each function gets the request args — request, response, ctx, params, query, body, session (not signals/stream/db/logger) — and can throw to stop the request.

This guard blocks the route unless the URL carries ?key=open — edit the URL in the demo to get through:

import { HttpError } from 'camelon';
import type { MiddlewareFunction } from 'camelon';

const requireKey: MiddlewareFunction = ({ query }) => {
  if (query.key !== 'open') {
    throw new HttpError(403, 'Blocked by middleware — add ?key=open to the URL');
  }
};

export const middleware = [requireKey];
export default function Guard() {
  return (
    <div class="d-card">
      <strong>You're past the guard ✅</strong>
      <p>
        The <code>_middleware.ts</code> in this folder ran first and let you through because the URL
        carried <code>?key=open</code>.
      </p>
    </div>
  );
}
live

A real auth guard reads the session instead:

import { redirect } from 'camelon';
import type { MiddlewareFunction } from 'camelon';

const requireAuth: MiddlewareFunction = ({ session }) => {
  if (!session.has('userId')) throw redirect('/login');
};

export const middleware = [requireAuth];
export default function Dashboard() {
  return <h1>Welcome back</h1>;
}

The _middleware.ts guard runs before Dashboard.tsx. If the session is empty it throws redirect, and the page never renders.

Guarding

Throw to stop the request before the handler runs:

throw redirect('/login');
throw notFound();

Passing data down

A value returned from middleware merges into ctx, the per-request scratch shared with the handler. It replaces Express's res.locals.

An app types itself in one place — the augmentable Register interface. Declare ctx to type the scratch, and session to key session.get/set:

// _middleware.ts
declare module 'camelon' {
  interface Register {
    ctx: { tenant: string };
    session: { userId: string };
  }
}

export const middleware: MiddlewareFunction[] = [
  ({ request }) => ({ tenant: tenantFrom(request) }), // → ctx.tenant
];
export type GetOutput = { userId: string | undefined; tenant: string };

export const get: GetFunction = ({ session, ctx }): GetOutput => {
  return { userId: session.get('userId'), tenant: ctx.tenant };
};

Read the session from session (get('userId') is typed from Register['session']). Read middleware values from ctx.