skills/farming-labs/fm-skills/middleware-patterns

middleware-patterns

SKILL.md

Middleware Patterns

Overview

Middleware is code that runs between receiving a request and sending a response. It's the backbone of request processing in web applications.

What is Middleware?

THE MIDDLEWARE CONCEPT:

REQUEST → [Middleware 1] → [Middleware 2] → [Middleware N] → HANDLER → RESPONSE
              ↓                 ↓                 ↓              ↓
          (logging)         (auth)           (parse)       (business logic)


MIDDLEWARE CAN:
├── Modify the request before it reaches the handler
├── Modify the response before it's sent
├── Short-circuit the chain (e.g., return 401 early)
├── Pass data to subsequent middleware/handlers
├── Perform side effects (logging, analytics)
└── Handle errors from downstream middleware

The Middleware Pipeline

REQUEST LIFECYCLE:

┌─────────────────────────────────────────────────────────────────┐
│                      INCOMING REQUEST                            │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│  MIDDLEWARE 1: Logging                                           │
│  ─────────────────────────────────────────────────────────────  │
│  console.log(`${method} ${url}`);                               │
│  const start = Date.now();                                      │
│  await next();  ────────────────────────┐                       │
│  console.log(`Completed in ${Date.now() - start}ms`);  ◄────────┘
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│  MIDDLEWARE 2: Authentication                                    │
│  ─────────────────────────────────────────────────────────────  │
│  const token = getHeader('Authorization');                      │
│  if (!token) return error(401);  // Short-circuit!              │
│  request.user = verifyToken(token);                             │
│  await next();  ────────────────────────┐                       │
│  // (response flows back through)        │                       │
└──────────────────────────────────────────┼──────────────────────┘
                              │            │
                              ▼            │
┌─────────────────────────────────────────────────────────────────┐
│  MIDDLEWARE 3: Rate Limiting                                     │
│  ─────────────────────────────────────────────────────────────  │
│  if (isRateLimited(request.ip)) return error(429);              │
│  await next();  ────────────────────────┐                       │
│                                          │                       │
└──────────────────────────────────────────┼──────────────────────┘
                              │            │
                              ▼            │
┌─────────────────────────────────────────────────────────────────┐
│                       ROUTE HANDLER                              │
│  ─────────────────────────────────────────────────────────────  │
│  return { data: 'Hello World' };                                │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                     OUTGOING RESPONSE                            │
│  (flows back through middleware in reverse order)               │
└─────────────────────────────────────────────────────────────────┘

Types of Middleware

MIDDLEWARE CLASSIFICATION:

BY SCOPE:
┌─────────────────────────────────────────────────────────────────┐
│  Global Middleware     │ Runs on every request                  │
│  Route Middleware      │ Runs on specific routes                │
│  Group Middleware      │ Runs on route groups                   │
│  Handler Middleware    │ Wraps specific handlers                │
└─────────────────────────────────────────────────────────────────┘

BY LOCATION:
┌─────────────────────────────────────────────────────────────────┐
│  Edge Middleware       │ Runs at CDN edge (before origin)       │
│  Server Middleware     │ Runs on origin server                  │
│  Client Middleware     │ Runs in browser (route guards)         │
└─────────────────────────────────────────────────────────────────┘

BY FUNCTION:
┌─────────────────────────────────────────────────────────────────┐
│  Request Middleware    │ Modifies incoming request              │
│  Response Middleware   │ Modifies outgoing response             │
│  Error Middleware      │ Handles errors                         │
│  Passthrough           │ Side effects only (logging)            │
└─────────────────────────────────────────────────────────────────┘

Common Middleware Patterns

Express-Style Middleware (Node.js)

// EXPRESS MIDDLEWARE PATTERN:
// (req, res, next) => void

// Logging middleware
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();  // Pass to next middleware
};

// Auth middleware
const auth = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = verifyToken(token);
  next();
};

// Error handling middleware (4 params)
const errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
};

// Usage
const app = express();
app.use(logger);           // Global
app.use('/api', auth);     // Path-specific
app.use(errorHandler);     // Error handler (must be last)

// Route-specific middleware
app.get('/admin', adminOnly, (req, res) => {
  res.json({ admin: true });
});

H3/Nitro Middleware (Universal)

// H3 MIDDLEWARE PATTERN:
// defineEventHandler((event) => { ... })

// server/middleware/logger.ts
export default defineEventHandler((event) => {
  console.log(`${event.method} ${event.path}`);
  // No return = pass through to next handler
});

// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const token = getHeader(event, 'authorization');
  
  // Only protect /api routes
  if (!event.path.startsWith('/api')) return;
  
  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized',
    });
  }
  
  event.context.user = verifyToken(token);
  // No return = continue to route handler
});

// server/middleware/timing.ts
export default defineEventHandler(async (event) => {
  const start = Date.now();
  
  // Run after response
  event.waitUntil(
    Promise.resolve().then(() => {
      console.log(`Request took ${Date.now() - start}ms`);
    })
  );
});

Edge Middleware (Vercel/Next.js)

// middleware.ts (at project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Redirect logic
  if (request.nextUrl.pathname === '/old-page') {
    return NextResponse.redirect(new URL('/new-page', request.url));
  }
  
  // Auth check
  const token = request.cookies.get('token');
  if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  // Add headers
  const response = NextResponse.next();
  response.headers.set('x-custom-header', 'my-value');
  
  // Geolocation (available at edge)
  const country = request.geo?.country || 'US';
  response.cookies.set('country', country);
  
  return response;
}

// Configure which paths run middleware
export const config = {
  matcher: [
    // Match all paths except static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

Cloudflare Workers Middleware

// Cloudflare Workers middleware pattern
export default {
  async fetch(request, env, ctx) {
    // Create middleware chain
    const middlewares = [
      corsMiddleware,
      authMiddleware,
      rateLimitMiddleware,
    ];
    
    // Execute chain
    let response;
    for (const middleware of middlewares) {
      response = await middleware(request, env, ctx);
      if (response) return response;  // Short-circuit
    }
    
    // Main handler
    return handleRequest(request, env);
  },
};

// CORS middleware
async function corsMiddleware(request) {
  if (request.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',
      },
    });
  }
  return null;  // Continue to next middleware
}

// Auth middleware
async function authMiddleware(request, env) {
  const url = new URL(request.url);
  
  if (url.pathname.startsWith('/api/protected')) {
    const token = request.headers.get('Authorization');
    if (!token) {
      return new Response('Unauthorized', { status: 401 });
    }
  }
  return null;  // Continue
}

Common Middleware Use Cases

1. Authentication & Authorization

// JWT Authentication
export default defineEventHandler(async (event) => {
  // Skip auth for public routes
  const publicRoutes = ['/api/login', '/api/register', '/api/health'];
  if (publicRoutes.includes(event.path)) return;
  
  const token = getHeader(event, 'authorization')?.replace('Bearer ', '');
  
  if (!token) {
    throw createError({ statusCode: 401, message: 'No token provided' });
  }
  
  try {
    const decoded = await verifyJWT(token);
    event.context.user = decoded;
  } catch (error) {
    throw createError({ statusCode: 401, message: 'Invalid token' });
  }
});

// Role-based Authorization
export default defineEventHandler((event) => {
  if (!event.path.startsWith('/api/admin')) return;
  
  const user = event.context.user;
  if (!user || user.role !== 'admin') {
    throw createError({ statusCode: 403, message: 'Admin access required' });
  }
});

2. CORS (Cross-Origin Resource Sharing)

// CORS Middleware
export default defineEventHandler((event) => {
  const origin = getHeader(event, 'origin');
  const allowedOrigins = ['https://example.com', 'https://app.example.com'];
  
  if (origin && allowedOrigins.includes(origin)) {
    setHeader(event, 'Access-Control-Allow-Origin', origin);
    setHeader(event, 'Access-Control-Allow-Credentials', 'true');
  }
  
  setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // Handle preflight
  if (event.method === 'OPTIONS') {
    setHeader(event, 'Access-Control-Max-Age', '86400');  // 24 hours
    return null;  // Return empty 200 response
  }
});

3. Rate Limiting

// Simple in-memory rate limiter
const requestCounts = new Map();

export default defineEventHandler((event) => {
  const ip = getHeader(event, 'x-forwarded-for') || 'unknown';
  const now = Date.now();
  const windowMs = 60 * 1000;  // 1 minute window
  const maxRequests = 100;
  
  // Get or create request record
  let record = requestCounts.get(ip);
  if (!record || now - record.start > windowMs) {
    record = { start: now, count: 0 };
    requestCounts.set(ip, record);
  }
  
  record.count++;
  
  // Set rate limit headers
  setHeader(event, 'X-RateLimit-Limit', maxRequests.toString());
  setHeader(event, 'X-RateLimit-Remaining', Math.max(0, maxRequests - record.count).toString());
  setHeader(event, 'X-RateLimit-Reset', (record.start + windowMs).toString());
  
  if (record.count > maxRequests) {
    throw createError({
      statusCode: 429,
      message: 'Too many requests',
    });
  }
});

4. Request Logging

// Structured logging middleware
export default defineEventHandler(async (event) => {
  const start = Date.now();
  const requestId = crypto.randomUUID();
  
  // Attach request ID
  event.context.requestId = requestId;
  setHeader(event, 'X-Request-ID', requestId);
  
  // Log request
  console.log(JSON.stringify({
    type: 'request',
    requestId,
    method: event.method,
    path: event.path,
    query: getQuery(event),
    userAgent: getHeader(event, 'user-agent'),
    ip: getHeader(event, 'x-forwarded-for'),
    timestamp: new Date().toISOString(),
  }));
  
  // Log response after completion
  event.waitUntil(
    Promise.resolve().then(() => {
      console.log(JSON.stringify({
        type: 'response',
        requestId,
        duration: Date.now() - start,
        status: event.node?.res?.statusCode || 200,
      }));
    })
  );
});

5. Response Compression

// Compression middleware (Node.js)
import { createGzip, createBrotliCompress } from 'zlib';

export default defineEventHandler((event) => {
  const acceptEncoding = getHeader(event, 'accept-encoding') || '';
  
  if (acceptEncoding.includes('br')) {
    event.context.compress = 'br';
    setHeader(event, 'Content-Encoding', 'br');
  } else if (acceptEncoding.includes('gzip')) {
    event.context.compress = 'gzip';
    setHeader(event, 'Content-Encoding', 'gzip');
  }
});

6. Security Headers

// Security headers middleware
export default defineEventHandler((event) => {
  // Prevent clickjacking
  setHeader(event, 'X-Frame-Options', 'DENY');
  
  // Prevent MIME type sniffing
  setHeader(event, 'X-Content-Type-Options', 'nosniff');
  
  // XSS protection
  setHeader(event, 'X-XSS-Protection', '1; mode=block');
  
  // Referrer policy
  setHeader(event, 'Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Content Security Policy
  setHeader(event, 'Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
  ].join('; '));
  
  // HSTS (HTTPS only)
  setHeader(event, 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
});

7. Request Validation

// Validation middleware using Zod
import { z } from 'zod';

// Factory function for validation middleware
function validateBody(schema) {
  return defineEventHandler(async (event) => {
    if (event.method === 'GET') return;
    
    const body = await readBody(event);
    const result = schema.safeParse(body);
    
    if (!result.success) {
      throw createError({
        statusCode: 400,
        message: 'Validation failed',
        data: result.error.flatten(),
      });
    }
    
    event.context.validatedBody = result.data;
  });
}

// Usage in route
const userSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

// server/api/users.post.ts
export default defineEventHandler({
  onRequest: [validateBody(userSchema)],
  handler: (event) => {
    const { name, email } = event.context.validatedBody;
    return createUser({ name, email });
  },
});

8. Caching

// Response caching middleware
export default defineEventHandler(async (event) => {
  // Only cache GET requests
  if (event.method !== 'GET') return;
  
  // Skip caching for authenticated requests
  if (getHeader(event, 'authorization')) return;
  
  const cacheKey = `cache:${event.path}:${JSON.stringify(getQuery(event))}`;
  const cached = await useStorage('cache').getItem(cacheKey);
  
  if (cached) {
    setHeader(event, 'X-Cache', 'HIT');
    return cached;
  }
  
  // Mark for caching after response
  event.context.cacheKey = cacheKey;
});

// In route handler
export default defineEventHandler(async (event) => {
  const data = await fetchExpensiveData();
  
  // Cache if middleware marked it
  if (event.context.cacheKey) {
    await useStorage('cache').setItem(event.context.cacheKey, data, {
      ttl: 3600,  // 1 hour
    });
  }
  
  return data;
});

Framework-Specific Middleware

Next.js Middleware

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Geo-based routing
  const country = request.geo?.country || 'US';
  if (country === 'EU') {
    return NextResponse.rewrite(new URL('/eu' + request.nextUrl.pathname, request.url));
  }
  
  // A/B testing
  const bucket = request.cookies.get('ab-bucket')?.value || 
    (Math.random() < 0.5 ? 'a' : 'b');
  
  const response = NextResponse.next();
  response.cookies.set('ab-bucket', bucket);
  
  // Bot detection
  const userAgent = request.headers.get('user-agent') || '';
  if (isBot(userAgent)) {
    // Serve pre-rendered version for bots
    return NextResponse.rewrite(new URL('/static' + request.nextUrl.pathname, request.url));
  }
  
  return response;
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Nuxt Middleware

// server/middleware/auth.ts (Server middleware - runs on server)
export default defineEventHandler((event) => {
  const token = getCookie(event, 'auth_token');
  if (token) {
    event.context.user = verifyToken(token);
  }
});

// middleware/auth.ts (Route middleware - runs on client navigation)
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useAuth();
  
  if (!user.value && to.path.startsWith('/dashboard')) {
    return navigateTo('/login');
  }
});

// Using route middleware in pages
// pages/dashboard.vue
definePageMeta({
  middleware: 'auth',  // or middleware: ['auth', 'admin']
});

SvelteKit Hooks

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  // Before route handler
  const token = event.cookies.get('token');
  if (token) {
    event.locals.user = await verifyToken(token);
  }
  
  // Call route handler
  const response = await resolve(event);
  
  // After route handler
  response.headers.set('X-Custom-Header', 'value');
  
  return response;
};

// Sequence multiple handlers
import { sequence } from '@sveltejs/kit/hooks';

export const handle = sequence(
  handleAuth,
  handleLogging,
  handleCompression
);

Remix Middleware Pattern

// Remix doesn't have traditional middleware
// Use loader/action patterns instead

// app/utils/auth.server.ts
export async function requireUser(request: Request) {
  const session = await getSession(request.headers.get('Cookie'));
  const userId = session.get('userId');
  
  if (!userId) {
    throw redirect('/login');
  }
  
  return await getUser(userId);
}

// app/routes/dashboard.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const user = await requireUser(request);  // "Middleware" pattern
  return json({ user });
}

// For cross-cutting concerns, use root loader
// app/root.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const env = {
    API_URL: process.env.API_URL,
  };
  const user = await getOptionalUser(request);
  
  return json({ env, user });
}

Edge Middleware vs Server Middleware

EDGE MIDDLEWARE:
┌─────────────────────────────────────────────────────────────────┐
│  Location:    CDN edge (globally distributed)                  │
│  Latency:     < 50ms (runs near user)                          │
│  Cold start:  < 5ms                                            │
│  Runtime:     V8 isolates (limited APIs)                       │
│  Use cases:   Auth, redirects, A/B tests, geolocation          │
│                                                                  │
│  RUNS BEFORE: Static files, serverless functions, origin       │
│                                                                  │
│  LIMITATIONS:                                                   │
│  - No Node.js APIs                                              │
│  - No database connections                                      │
│  - Limited execution time (usually < 30s)                       │
│  - Limited memory                                               │
└─────────────────────────────────────────────────────────────────┘

SERVER MIDDLEWARE:
┌─────────────────────────────────────────────────────────────────┐
│  Location:    Origin server (single region)                    │
│  Latency:     50-200ms (depends on user distance)              │
│  Cold start:  100ms-3s (serverless) or 0 (traditional)         │
│  Runtime:     Node.js (full APIs)                              │
│  Use cases:   DB access, complex auth, heavy computation       │
│                                                                  │
│  RUNS AFTER:  CDN cache, edge middleware                       │
│                                                                  │
│  CAPABILITIES:                                                  │
│  - Full Node.js ecosystem                                       │
│  - Database connections                                         │
│  - File system access                                           │
│  - Long execution times                                         │
└─────────────────────────────────────────────────────────────────┘

WHEN TO USE EDGE:
├─► Authentication checks (JWT validation)
├─► Redirects (marketing, localization)
├─► A/B testing (cookie-based routing)
├─► Geolocation-based personalization
├─► Bot detection and blocking
├─► Header manipulation
└─► Feature flags

WHEN TO USE SERVER:
├─► Database queries
├─► Complex business logic
├─► Third-party API calls requiring secrets
├─► File processing
├─► Session management with storage
└─► Heavy computation

Middleware Composition Patterns

Chain Pattern

// Composing middleware into a chain
function createMiddlewareChain(...middlewares) {
  return async (request, context) => {
    let index = 0;
    
    async function next() {
      if (index < middlewares.length) {
        const middleware = middlewares[index++];
        return await middleware(request, context, next);
      }
    }
    
    return await next();
  };
}

// Usage
const chain = createMiddlewareChain(
  loggingMiddleware,
  authMiddleware,
  rateLimitMiddleware
);

export default {
  fetch: (request) => chain(request, {}),
};

Pipeline Pattern

// Functional pipeline approach
const pipe = (...fns) => (value) =>
  fns.reduce((acc, fn) => acc.then(fn), Promise.resolve(value));

const pipeline = pipe(
  addRequestId,
  validateAuth,
  checkRateLimit,
  handleRequest
);

async function addRequestId(event) {
  event.context.requestId = crypto.randomUUID();
  return event;
}

async function validateAuth(event) {
  // Throws if invalid
  event.context.user = await authenticate(event);
  return event;
}

Decorator Pattern

// Middleware as decorators (TypeScript)
function withAuth(handler: EventHandler): EventHandler {
  return defineEventHandler(async (event) => {
    const user = await authenticate(event);
    if (!user) {
      throw createError({ statusCode: 401 });
    }
    event.context.user = user;
    return handler(event);
  });
}

function withCache(ttl: number) {
  return (handler: EventHandler): EventHandler => {
    return defineEventHandler(async (event) => {
      const cached = await getCache(event);
      if (cached) return cached;
      
      const result = await handler(event);
      await setCache(event, result, ttl);
      return result;
    });
  };
}

// Usage
export default withAuth(
  withCache(3600)(
    defineEventHandler((event) => {
      return { data: 'protected and cached' };
    })
  )
);

Deep Dive: Understanding Middleware Internals

The Onion Model

MIDDLEWARE EXECUTION ORDER (Onion Model):

                    REQUEST
        ┌──────────────────────────────────┐
        │  ┌────────────────────────────┐  │
        │  │  ┌──────────────────────┐  │  │
        │  │  │  ┌────────────────┐  │  │  │
        │  │  │  │                │  │  │  │
        │  │  │  │    HANDLER     │  │  │  │
        │  │  │  │                │  │  │  │
        │  │  │  └────────────────┘  │  │  │
        │  │  │    Middleware 3      │  │  │
        │  │  └──────────────────────┘  │  │
        │  │      Middleware 2          │  │
        │  └────────────────────────────┘  │
        │        Middleware 1              │
        └──────────────────────────────────┘
                   RESPONSE


EXECUTION FLOW:

1. Middleware 1: ENTER (request phase)
2. Middleware 2: ENTER (request phase)
3. Middleware 3: ENTER (request phase)
4. Handler: EXECUTE
5. Middleware 3: EXIT (response phase)
6. Middleware 2: EXIT (response phase)
7. Middleware 1: EXIT (response phase)

CODE EXAMPLE:

async function middleware1(request, next) {
  console.log('1: entering');      // Step 1
  const response = await next();
  console.log('1: exiting');       // Step 7
  return response;
}

async function middleware2(request, next) {
  console.log('2: entering');      // Step 2
  const response = await next();
  console.log('2: exiting');       // Step 6
  return response;
}

async function middleware3(request, next) {
  console.log('3: entering');      // Step 3
  const response = await next();
  console.log('3: exiting');       // Step 5
  return response;
}

async function handler(request) {
  console.log('handler');          // Step 4
  return new Response('OK');
}

How next() Works

// SIMPLIFIED IMPLEMENTATION OF next():

function createMiddlewareRunner(middlewares, handler) {
  return async function runner(request) {
    let index = 0;
    
    async function dispatch(i) {
      // Prevent calling next() multiple times
      if (i <= index) {
        throw new Error('next() called multiple times');
      }
      index = i;
      
      // Get current middleware or handler
      let fn;
      if (i < middlewares.length) {
        fn = middlewares[i];
      } else if (i === middlewares.length) {
        fn = handler;
      } else {
        return;  // No more middleware
      }
      
      // Create next function for this middleware
      const next = () => dispatch(i + 1);
      
      // Execute middleware
      return await fn(request, next);
    }
    
    return dispatch(0);
  };
}

// USAGE:
const run = createMiddlewareRunner(
  [logger, auth, rateLimit],
  handler
);

const response = await run(request);


// WHAT HAPPENS:

// dispatch(0) - logger middleware
//   └─► calls next()
//       └─► dispatch(1) - auth middleware
//           └─► calls next()
//               └─► dispatch(2) - rateLimit middleware
//                   └─► calls next()
//                       └─► dispatch(3) - handler
//                           └─► returns response
//                       └─► returns response
//                   └─► returns response
//               └─► returns response
//           └─► returns response
//       └─► returns response
//   └─► returns response

Error Handling in Middleware

// ERRORS PROPAGATE BACK THROUGH THE CHAIN:

async function errorHandlingMiddleware(request, next) {
  try {
    return await next();
  } catch (error) {
    // Handle error
    console.error('Error:', error);
    
    // Transform to response
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: error.statusCode || 500 }
    );
  }
}

// ERROR PROPAGATION:

// 1. Middleware 1: enter
// 2. Middleware 2: enter
// 3. Handler: throws Error!
// 4. Middleware 2: catch (if try/catch) OR propagate up
// 5. Middleware 1: catch (if try/catch) OR propagate up
// 6. Framework error handler

// EXPRESS ERROR MIDDLEWARE:
// Special 4-parameter signature
app.use((err, req, res, next) => {
  // Only called if error occurred
  console.error(err);
  res.status(500).json({ error: 'Something went wrong' });
});

// H3 ERROR HANDLING:
// Uses createError and error handlers
export default defineEventHandler((event) => {
  throw createError({
    statusCode: 400,
    message: 'Bad Request',
    data: { field: 'email' },  // Additional data
  });
});

// Global error handler in Nitro
// nitro.config.ts
export default defineNitroConfig({
  errorHandler: '~/error-handler.ts',
});

// error-handler.ts
export default defineNitroErrorHandler((error, event) => {
  setResponseStatus(event, error.statusCode || 500);
  return { error: error.message };
});

Context Passing

// HOW DATA FLOWS THROUGH MIDDLEWARE:

// 1. REQUEST CONTEXT PATTERN (Recommended)
// Attach data to request/event context

async function authMiddleware(request, next) {
  const user = await authenticate(request);
  request.context.user = user;  // Attach to context
  return next();
}

async function handler(request) {
  const user = request.context.user;  // Access later
  return Response.json({ user });
}


// 2. HEADERS PATTERN
// Pass data via headers (good for edge → origin)

async function edgeMiddleware(request) {
  const country = request.geo.country;
  request.headers.set('X-User-Country', country);
  return fetch(request);  // Forward to origin
}


// 3. ASYNC LOCAL STORAGE (Node.js)
// Thread-local storage for request scope

import { AsyncLocalStorage } from 'async_hooks';

const requestContext = new AsyncLocalStorage();

function middleware(req, res, next) {
  const context = { user: null, requestId: crypto.randomUUID() };
  requestContext.run(context, () => next());
}

// Anywhere in the request lifecycle:
function getRequestContext() {
  return requestContext.getStore();
}


// 4. UNCTX (UnJS Context)
// Similar to AsyncLocalStorage but universal

import { createContext } from 'unctx';

const eventContext = createContext();

export const useEvent = eventContext.use;

// In middleware
eventContext.call(event, () => handler());

// In any function
const event = useEvent();  // Get current event

Performance Considerations

MIDDLEWARE PERFORMANCE IMPACT:

1. CHAIN LENGTH
   - Each middleware adds latency
   - More middleware = more function calls
   - 10 middleware × 1ms each = 10ms overhead

2. ASYNC OPERATIONS
   - await in middleware blocks the chain
   - Database calls in middleware slow EVERY request
   - Use caching, lazy loading

3. MEMORY ALLOCATIONS
   - Creating objects per request
   - Closures capture variables
   - GC pressure from high traffic


OPTIMIZATION STRATEGIES:

// 1. SHORT-CIRCUIT EARLY
async function rateLimitMiddleware(request, next) {
  if (isRateLimited(request.ip)) {
    return new Response('Too Many Requests', { status: 429 });
  }
  return next();  // Only call next if not limited
}

// 2. LAZY MIDDLEWARE EXECUTION
async function conditionalAuth(request, next) {
  // Only run auth logic for protected routes
  if (!request.url.includes('/api/protected')) {
    return next();  // Skip auth entirely
  }
  // ... auth logic
}

// 3. PARALLEL OPERATIONS
async function aggregatingMiddleware(request, next) {
  // Don't await sequentially if not dependent
  const [user, config] = await Promise.all([
    getUser(request),
    getConfig(),
  ]);
  request.context.user = user;
  request.context.config = config;
  return next();
}

// 4. CACHE MIDDLEWARE RESULTS
const middlewareCache = new Map();

async function cachedMiddleware(request, next) {
  const cacheKey = getCacheKey(request);
  
  if (middlewareCache.has(cacheKey)) {
    request.context.data = middlewareCache.get(cacheKey);
    return next();
  }
  
  const data = await expensiveOperation();
  middlewareCache.set(cacheKey, data);
  request.context.data = data;
  return next();
}

Middleware Order Matters

COMMON MIDDLEWARE ORDER:

1. Error Handler (wrap everything)
2. Request ID / Tracing
3. Logging (log all requests)
4. Security Headers
5. CORS
6. Compression
7. Rate Limiting
8. Authentication
9. Authorization
10. Validation
11. Caching
12. → Route Handler


WHY ORDER MATTERS:

WRONG ORDER:
[Auth] → [RateLimit] → [Logging]
└─► Rate limit bypassed by failed auth
└─► Failed requests not logged

CORRECT ORDER:
[Logging] → [RateLimit] → [Auth]
└─► All requests logged
└─► Rate limit applied before auth (protect from brute force)
└─► Auth checked after rate limit


EXAMPLE CONFIGURATION:

// Nitro/H3 - file naming controls order
server/middleware/
├── 01.logging.ts       // First (prefix controls order)
├── 02.security.ts
├── 03.cors.ts
├── 04.rate-limit.ts
├── 05.auth.ts
└── 06.validation.ts

// Express - use() order matters
app.use(logging);
app.use(security);
app.use(cors);
app.use(rateLimit);
app.use(auth);
app.use(validation);

Testing Middleware

// UNIT TESTING MIDDLEWARE:

import { describe, it, expect, vi } from 'vitest';

// Mock event for H3
function createMockEvent(options = {}) {
  return {
    method: options.method || 'GET',
    path: options.path || '/',
    headers: new Headers(options.headers || {}),
    context: {},
  };
}

describe('authMiddleware', () => {
  it('should set user in context when token is valid', async () => {
    const event = createMockEvent({
      headers: { authorization: 'Bearer valid-token' },
    });
    
    await authMiddleware(event);
    
    expect(event.context.user).toBeDefined();
    expect(event.context.user.id).toBe('123');
  });
  
  it('should throw 401 when no token', async () => {
    const event = createMockEvent();
    
    await expect(authMiddleware(event)).rejects.toMatchObject({
      statusCode: 401,
    });
  });
});

// INTEGRATION TESTING MIDDLEWARE CHAIN:

import { createApp, createRouter, toNodeHandler } from 'h3';
import supertest from 'supertest';

describe('middleware chain', () => {
  const app = createApp();
  app.use(loggingMiddleware);
  app.use(authMiddleware);
  app.use(router);
  
  const request = supertest(toNodeHandler(app));
  
  it('should allow authenticated requests', async () => {
    const response = await request
      .get('/api/protected')
      .set('Authorization', 'Bearer valid-token');
    
    expect(response.status).toBe(200);
  });
  
  it('should reject unauthenticated requests', async () => {
    const response = await request.get('/api/protected');
    
    expect(response.status).toBe(401);
  });
});

For Framework Authors: Building Middleware Systems

Implementation Note: The patterns and code examples below represent one proven approach to building middleware systems. Middleware patterns vary—Express uses a callback-based approach, Koa uses async/await with context, and H3 uses a minimal event-based system. The direction shown here follows the modern async/await pattern with composability. Adapt based on your framework's async model, error handling strategy, and whether you need framework-specific integrations.

Implementing a Middleware Pipeline

// CORE MIDDLEWARE EXECUTION ENGINE

class MiddlewarePipeline {
  constructor() {
    this.middlewares = [];
    this.errorHandlers = [];
  }
  
  // Register middleware
  use(path, ...handlers) {
    // If first arg is a function, it's a global middleware
    if (typeof path === 'function') {
      handlers = [path, ...handlers];
      path = '/';
    }
    
    for (const handler of handlers) {
      this.middlewares.push({
        path: this.normalizePath(path),
        handler,
        isError: handler.length === 4, // (error, ctx, next)
      });
    }
    
    return this;
  }
  
  // Register error handler
  onError(handler) {
    this.errorHandlers.push(handler);
    return this;
  }
  
  normalizePath(path) {
    if (path === '/') return '';
    return path.replace(/\/$/, '');
  }
  
  // Create request handler
  handler() {
    return async (request, context = {}) => {
      const ctx = this.createContext(request, context);
      
      try {
        await this.execute(ctx);
        
        // If no response set, return 404
        if (!ctx.response) {
          ctx.response = new Response('Not Found', { status: 404 });
        }
        
        return ctx.response;
      } catch (error) {
        return this.handleError(error, ctx);
      }
    };
  }
  
  createContext(request, extra = {}) {
    const url = new URL(request.url);
    
    return {
      request,
      url,
      path: url.pathname,
      method: request.method,
      headers: request.headers,
      query: Object.fromEntries(url.searchParams),
      params: {},
      state: {},
      response: null,
      ...extra,
      
      // Helper methods
      json(data, status = 200) {
        this.response = Response.json(data, { status });
      },
      
      text(body, status = 200) {
        this.response = new Response(body, { status });
      },
      
      redirect(url, status = 302) {
        this.response = Response.redirect(url, status);
      },
      
      set(key, value) {
        this.state[key] = value;
      },
      
      get(key) {
        return this.state[key];
      },
    };
  }
  
  async execute(ctx) {
    const applicableMiddlewares = this.middlewares.filter(m => 
      !m.isError && ctx.path.startsWith(m.path)
    );
    
    let index = 0;
    
    const next = async () => {
      if (index >= applicableMiddlewares.length) return;
      
      const middleware = applicableMiddlewares[index++];
      await middleware.handler(ctx, next);
    };
    
    await next();
  }
  
  async handleError(error, ctx) {
    // Try error handlers
    for (const handler of this.errorHandlers) {
      try {
        await handler(error, ctx);
        if (ctx.response) return ctx.response;
      } catch (e) {
        error = e;
      }
    }
    
    // Try error middlewares
    const errorMiddlewares = this.middlewares.filter(m => m.isError);
    for (const { handler } of errorMiddlewares) {
      try {
        await handler(error, ctx, () => {});
        if (ctx.response) return ctx.response;
      } catch (e) {
        error = e;
      }
    }
    
    // Default error response
    console.error('Unhandled error:', error);
    return new Response(
      JSON.stringify({ error: 'Internal Server Error' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }
}

// Usage
const app = new MiddlewarePipeline();

app.use(async (ctx, next) => {
  console.log(`${ctx.method} ${ctx.path}`);
  await next();
});

app.use('/api', async (ctx, next) => {
  ctx.set('apiVersion', 'v1');
  await next();
});

export default app.handler();

Composable Middleware Factories

// MIDDLEWARE COMPOSITION PATTERNS

// Higher-order middleware (factory pattern)
function withAuth(options = {}) {
  const { 
    header = 'Authorization',
    scheme = 'Bearer',
    verify = async (token) => { throw new Error('Must implement verify'); },
    onError = (ctx) => ctx.json({ error: 'Unauthorized' }, 401),
  } = options;
  
  return async (ctx, next) => {
    const authHeader = ctx.headers.get(header);
    
    if (!authHeader?.startsWith(`${scheme} `)) {
      return onError(ctx);
    }
    
    const token = authHeader.slice(scheme.length + 1);
    
    try {
      const user = await verify(token);
      ctx.set('user', user);
      await next();
    } catch (error) {
      return onError(ctx);
    }
  };
}

// Compose multiple middlewares
function compose(...middlewares) {
  return async (ctx, next) => {
    let index = -1;
    
    async function dispatch(i) {
      if (i <= index) {
        throw new Error('next() called multiple times');
      }
      index = i;
      
      let fn = middlewares[i];
      if (i === middlewares.length) fn = next;
      if (!fn) return;
      
      await fn(ctx, dispatch.bind(null, i + 1));
    }
    
    return dispatch(0);
  };
}

// Conditional middleware
function when(condition, middleware) {
  return async (ctx, next) => {
    const shouldRun = typeof condition === 'function' 
      ? await condition(ctx) 
      : condition;
    
    if (shouldRun) {
      await middleware(ctx, next);
    } else {
      await next();
    }
  };
}

// Path-scoped middleware
function scope(path, ...middlewares) {
  const composed = compose(...middlewares);
  
  return async (ctx, next) => {
    if (ctx.path.startsWith(path)) {
      await composed(ctx, next);
    } else {
      await next();
    }
  };
}

// Usage
const apiMiddleware = compose(
  withAuth({ verify: verifyJWT }),
  when(ctx => ctx.method === 'POST', validateBody),
  rateLimiter({ max: 100, window: 60000 }),
);

app.use('/api', apiMiddleware);

Route-Specific Middleware Integration

// INTEGRATING MIDDLEWARE WITH ROUTER

class Router {
  constructor() {
    this.routes = [];
    this.middlewares = [];
  }
  
  use(...middlewares) {
    this.middlewares.push(...middlewares);
    return this;
  }
  
  route(method, path, ...handlers) {
    const middleware = handlers.slice(0, -1);
    const handler = handlers[handlers.length - 1];
    
    this.routes.push({
      method: method.toUpperCase(),
      pattern: this.compile(path),
      middleware,
      handler,
    });
    
    return this;
  }
  
  get(path, ...handlers) { return this.route('GET', path, ...handlers); }
  post(path, ...handlers) { return this.route('POST', path, ...handlers); }
  put(path, ...handlers) { return this.route('PUT', path, ...handlers); }
  delete(path, ...handlers) { return this.route('DELETE', path, ...handlers); }
  
  compile(path) {
    const params = [];
    const regex = path.replace(/:(\w+)/g, (_, name) => {
      params.push(name);
      return '([^/]+)';
    });
    
    return {
      regex: new RegExp(`^${regex}$`),
      params,
    };
  }
  
  match(method, path) {
    for (const route of this.routes) {
      if (route.method !== method && route.method !== 'ALL') continue;
      
      const match = path.match(route.pattern.regex);
      if (match) {
        const params = {};
        route.pattern.params.forEach((name, i) => {
          params[name] = match[i + 1];
        });
        
        return { route, params };
      }
    }
    
    return null;
  }
  
  // Convert to middleware
  routes() {
    return async (ctx, next) => {
      const matched = this.match(ctx.method, ctx.path);
      
      if (!matched) {
        return next();
      }
      
      ctx.params = matched.params;
      
      // Combine all middlewares
      const allMiddleware = [
        ...this.middlewares,
        ...matched.route.middleware,
        matched.route.handler,
      ];
      
      await compose(...allMiddleware)(ctx, next);
    };
  }
}

// Usage
const userRouter = new Router();

userRouter.use(withAuth());

userRouter.get('/users', async (ctx) => {
  ctx.json({ users: await getUsers() });
});

userRouter.get('/users/:id', 
  validateParams({ id: 'number' }),
  async (ctx) => {
    const user = await getUser(ctx.params.id);
    ctx.json(user);
  }
);

app.use('/api', userRouter.routes());

Async Context Propagation

// ASYNC LOCAL STORAGE FOR MIDDLEWARE CONTEXT

import { AsyncLocalStorage } from 'node:async_hooks';

// Create storage for request context
const requestContext = new AsyncLocalStorage();

// Middleware to initialize context
function contextMiddleware() {
  return async (ctx, next) => {
    const store = {
      requestId: crypto.randomUUID(),
      startTime: Date.now(),
      user: null,
      tracing: {},
    };
    
    // Run rest of middleware chain within context
    await requestContext.run(store, async () => {
      await next();
    });
  };
}

// Get current request context (works anywhere in call stack)
function getContext() {
  return requestContext.getStore();
}

// Helper to get specific values
function getRequestId() {
  return getContext()?.requestId;
}

function getCurrentUser() {
  return getContext()?.user;
}

function setCurrentUser(user) {
  const ctx = getContext();
  if (ctx) ctx.user = user;
}

// Usage in middleware
const authMiddleware = async (ctx, next) => {
  const user = await verifyToken(ctx.headers.get('authorization'));
  setCurrentUser(user); // Available anywhere in request
  await next();
};

// Usage in any function (no need to pass context)
async function createOrder(data) {
  const user = getCurrentUser(); // Works!
  const requestId = getRequestId();
  
  console.log(`[${requestId}] Creating order for user ${user.id}`);
  
  return await db.orders.create({
    ...data,
    userId: user.id,
    createdAt: new Date(),
  });
}

// For edge runtimes without AsyncLocalStorage
class EdgeContext {
  static storage = new Map();
  
  static run(ctx, id, fn) {
    this.storage.set(id, ctx);
    try {
      return fn();
    } finally {
      this.storage.delete(id);
    }
  }
  
  static get(id) {
    return this.storage.get(id);
  }
}

Plugin-Based Middleware System

// EXTENSIBLE MIDDLEWARE PLUGIN ARCHITECTURE

class MiddlewarePlugin {
  constructor(name) {
    this.name = name;
    this.hooks = {};
  }
  
  // Lifecycle hooks
  onInit(fn) { this.hooks.init = fn; return this; }
  onRequest(fn) { this.hooks.request = fn; return this; }
  onResponse(fn) { this.hooks.response = fn; return this; }
  onError(fn) { this.hooks.error = fn; return this; }
  onDestroy(fn) { this.hooks.destroy = fn; return this; }
  
  // Convert to middleware
  toMiddleware() {
    return async (ctx, next) => {
      // Request phase
      if (this.hooks.request) {
        await this.hooks.request(ctx);
      }
      
      try {
        await next();
        
        // Response phase
        if (this.hooks.response) {
          await this.hooks.response(ctx);
        }
      } catch (error) {
        // Error phase
        if (this.hooks.error) {
          await this.hooks.error(error, ctx);
        } else {
          throw error;
        }
      }
    };
  }
}

// Plugin registry
class PluginRegistry {
  constructor() {
    this.plugins = new Map();
  }
  
  register(plugin) {
    this.plugins.set(plugin.name, plugin);
    return this;
  }
  
  async init(config) {
    for (const plugin of this.plugins.values()) {
      if (plugin.hooks.init) {
        await plugin.hooks.init(config);
      }
    }
  }
  
  getMiddlewares() {
    return [...this.plugins.values()].map(p => p.toMiddleware());
  }
  
  async destroy() {
    for (const plugin of this.plugins.values()) {
      if (plugin.hooks.destroy) {
        await plugin.hooks.destroy();
      }
    }
  }
}

// Example: Logging plugin
const loggingPlugin = new MiddlewarePlugin('logging')
  .onInit((config) => {
    console.log('Logging plugin initialized');
  })
  .onRequest((ctx) => {
    ctx.set('startTime', Date.now());
    console.log(`${ctx.method} ${ctx.path}`);
  })
  .onResponse((ctx) => {
    const duration = Date.now() - ctx.get('startTime');
    console.log(`${ctx.response?.status} (${duration}ms)`);
  })
  .onError((error, ctx) => {
    console.error(`✕ Error: ${error.message}`);
    throw error; // Re-throw to let error handlers deal with it
  });

// Example: Metrics plugin
const metricsPlugin = new MiddlewarePlugin('metrics')
  .onInit(async (config) => {
    // Initialize metrics client
  })
  .onResponse((ctx) => {
    const duration = Date.now() - ctx.get('startTime');
    // Record metrics
    metrics.histogram('http_request_duration', duration, {
      method: ctx.method,
      path: ctx.path,
      status: ctx.response?.status,
    });
  });

// Usage
const registry = new PluginRegistry();
registry.register(loggingPlugin);
registry.register(metricsPlugin);

await registry.init(config);

const app = new MiddlewarePipeline();
app.use(...registry.getMiddlewares());

Testing Middleware

// MIDDLEWARE TESTING UTILITIES

class MockContext {
  constructor(options = {}) {
    this.method = options.method || 'GET';
    this.path = options.path || '/';
    this.headers = new Headers(options.headers);
    this.query = options.query || {};
    this.params = options.params || {};
    this.body = options.body;
    this.state = {};
    this.response = null;
  }
  
  json(data, status = 200) {
    this.response = { type: 'json', data, status };
  }
  
  text(body, status = 200) {
    this.response = { type: 'text', body, status };
  }
  
  set(key, value) {
    this.state[key] = value;
  }
  
  get(key) {
    return this.state[key];
  }
}

// Test helper
async function testMiddleware(middleware, options = {}) {
  const ctx = new MockContext(options);
  let nextCalled = false;
  let nextError = null;
  
  const next = async () => {
    nextCalled = true;
    if (options.nextThrows) {
      throw options.nextThrows;
    }
  };
  
  try {
    await middleware(ctx, next);
  } catch (error) {
    nextError = error;
  }
  
  return {
    ctx,
    nextCalled,
    error: nextError,
  };
}

// Tests
describe('authMiddleware', () => {
  it('passes authenticated requests', async () => {
    const { ctx, nextCalled, error } = await testMiddleware(
      withAuth({ verify: async () => ({ id: 1, name: 'Test' }) }),
      {
        headers: { authorization: 'Bearer valid-token' },
      }
    );
    
    expect(nextCalled).toBe(true);
    expect(error).toBeNull();
    expect(ctx.state.user).toEqual({ id: 1, name: 'Test' });
  });
  
  it('blocks unauthenticated requests', async () => {
    const { ctx, nextCalled } = await testMiddleware(
      withAuth({ verify: async () => ({ id: 1 }) }),
      { headers: {} }
    );
    
    expect(nextCalled).toBe(false);
    expect(ctx.response.status).toBe(401);
  });
  
  it('handles verification errors', async () => {
    const { ctx, nextCalled } = await testMiddleware(
      withAuth({ verify: async () => { throw new Error('Invalid'); } }),
      { headers: { authorization: 'Bearer bad-token' } }
    );
    
    expect(nextCalled).toBe(false);
    expect(ctx.response.status).toBe(401);
  });
});

Related Skills

Weekly Installs
2
GitHub Stars
31
First Seen
Feb 6, 2026
Installed on
opencode2
gemini-cli2
claude-code2
github-copilot2
codex2
kimi-cli2