middleware-patterns

Explains middleware concepts, patterns, and implementations. Covers server middleware, edge middleware, request/response pipelines, and common use cases like auth, logging, and CORS. Use when implementing middleware or understanding request processing pipelines.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "middleware-patterns" with this command: npx skills add farming-labs/fm-skills/farming-labs-fm-skills-middleware-patterns

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

web-app-architectures

No summary provided by upstream source.

Repository SourceNeeds Review
General

build-pipelines-bundling

No summary provided by upstream source.

Repository SourceNeeds Review
General

middleware-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

data-fetching-patterns

No summary provided by upstream source.

Repository SourceNeeds Review