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
- See universal-javascript-runtimes for H3 and Nitro middleware
- See routing-patterns for route-specific middleware
- See meta-frameworks-overview for framework-specific middleware
More from kinfe123/fm-skills
meta-frameworks-overview
Explains meta-frameworks like Next.js, Nuxt, SvelteKit, Remix, Astro, and their architectural patterns. Use when comparing frameworks, choosing a framework for a project, or understanding what features meta-frameworks provide beyond base UI libraries.
11routing-patterns
Explains client-side routing, server-side routing, file-based routing, and navigation patterns in web applications. Use when implementing routing, understanding how navigation works in SPAs vs MPAs, or configuring routes in meta-frameworks.
9rendering-patterns
Explains web rendering strategies including Client-Side Rendering (CSR), Server-Side Rendering (SSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), and streaming. Use when deciding rendering strategy, optimizing performance, or understanding when content is generated and sent to users.
9web-app-architectures
Explains Single Page Applications (SPA), Multi Page Applications (MPA), and hybrid architectures. Use when discussing app architecture decisions, comparing SPA vs MPA, explaining how traditional vs modern web apps work, or when building a new web application.
9seo-fundamentals
Explains SEO for web applications including crawling, indexing, Core Web Vitals, structured data, and SEO challenges with SPAs. Use when optimizing for search engines, discussing SEO implications of architecture decisions, or implementing SEO best practices.
5build-pipelines-bundling
Explains JavaScript bundling, code splitting, chunking strategies, tree shaking, and build pipelines. Use when optimizing bundle size, understanding how modern build tools work, configuring Webpack/Vite/esbuild, or debugging build output.
5