GitHubnpm

Middleware

Juice middleware uses an onion model. Each middleware receives the request and a next() function. Call next() to continue down the chain, or return a Response to short-circuit.

Onion Model

Request
  → root middleware.ts
    → admin/middleware.ts
      → admin/dashboard.tsx (route)
    ← admin/middleware.ts (after next())
  ← root middleware.ts (after next())
Response

Middleware files are discovered per directory and chain from root to leaf. Each middleware runs before all routes in its directory and subdirectories. The next() call is one-shot to prevent double-rendering.

Typed Context with setContext / getContext

Pass data from middleware to route components using the request-scoped context store. Use createContextKey for compile-time type safety.

// app/context-keys.ts
import { createContextKey } from '@cmj/juice/runtime';
import type { User, Database } from './types';

export const userKey = createContextKey<User>('user');
export const dbKey = createContextKey<Database>('db');

Auth Pattern

// app/routes/admin/middleware.ts
import { setContext, getContext } from '@cmj/juice/runtime';
import { userKey } from '../../context-keys';

export default async function authMiddleware(
  req: Request,
  next: () => Promise<Response>,
) {
  const token = req.headers.get('authorization')?.replace('Bearer ', '');
  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }

  const user = await verifyToken(token);
  if (!user) {
    return new Response('Forbidden', { status: 403 });
  }

  setContext(req, userKey, user);
  return next();
}

// In a route component — type is inferred as User | undefined
// app/routes/admin/dashboard.tsx
import { getContext } from '@cmj/juice/runtime';
import { userKey } from '../../context-keys';

export default function Dashboard({ request }: { request: Request }) {
  const user = getContext(request, userKey); // User | undefined
  return <h1>Welcome, {user?.name}</h1>;
}

CORS Pattern

// app/routes/api/middleware.ts
export default async function corsMiddleware(
  req: Request,
  next: () => Promise<Response>,
) {
  // Handle preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      },
    });
  }

  const response = await next();

  // Add CORS headers to all API responses
  response.headers.set('Access-Control-Allow-Origin', '*');
  return response;
}

Request Logging

// app/routes/middleware.ts
export default async function loggingMiddleware(
  req: Request,
  next: () => Promise<Response>,
) {
  const start = performance.now();
  const response = await next();
  const ms = (performance.now() - start).toFixed(1);

  console.log(`${req.method} ${new URL(req.url).pathname} ${response.status} ${ms}ms`);
  return response;
}

Database Connection Pooling

// app/routes/middleware.ts
import { setContext } from '@cmj/juice/runtime';
import { dbKey } from '../context-keys';

const pool = createPool({ connectionString: process.env.DATABASE_URL });

export default async function dbMiddleware(
  req: Request,
  next: () => Promise<Response>,
) {
  const db = await pool.connect();
  setContext(req, dbKey, db);

  try {
    return await next();
  } finally {
    db.release();
  }
}