Middleware
Middleware are functions that execute before your route handlers, allowing you to implement cross-cutting concerns like authentication, logging, rate limiting, and more.
Middleware Concepts
Middleware Function
A middleware is an async function that receives a context and returns a response:
type Middleware = (
context: MiddlewareContext
) => Promise<MiddlewareResponse> | MiddlewareResponse;
Context Object
Every middleware receives a context with request information:
interface MiddlewareContext {
request: Request; // The incoming request
params: Record<string, string>; // Route parameters
searchParams: URLSearchParams; // Query parameters
pathname: string; // The current pathname
}
Response Object
Middleware must return a response indicating whether to continue:
interface MiddlewareResponse {
continue: boolean; // Whether to continue to next middleware
redirect?: string; // Optional redirect URL
headers?: Record<string, string>; // Headers to add
data?: any; // Data to pass to route/component
}
Creating Custom Middleware
Basic Example
const loggerMiddleware: Middleware = async (context) => {
console.log(`${context.request.method} ${context.pathname}`);
return {
continue: true,
data: { timestamp: new Date().toISOString() }
};
};
Authentication Middleware
const authMiddleware = (redirectTo: string = '/login'): Middleware => {
return async (context) => {
const authHeader = context.request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return {
continue: false,
redirect: redirectTo
};
}
// Validate token (implement your own logic)
const token = authHeader.replace('Bearer ', '');
const user = await validateToken(token);
if (!user) {
return {
continue: false,
redirect: redirectTo
};
}
return {
continue: true,
data: { user }
};
};
};
Rate Limiting Middleware
const rateLimitMiddleware = (
maxRequests: number = 100,
windowMs: number = 60000
): Middleware => {
const requests = new Map<string, { count: number; resetTime: number }>();
return async (context) => {
const clientId = context.request.headers.get('x-forwarded-for') ||
context.request.headers.get('x-real-ip') ||
'unknown';
const now = Date.now();
const clientData = requests.get(clientId) || { count: 0, resetTime: now + windowMs };
if (now > clientData.resetTime) {
clientData.count = 0;
clientData.resetTime = now + windowMs;
}
clientData.count++;
requests.set(clientId, clientData);
if (clientData.count > maxRequests) {
return {
continue: false,
headers: {
'X-RateLimit-Limit': maxRequests.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': clientData.resetTime.toString(),
}
};
}
return {
continue: true,
headers: {
'X-RateLimit-Limit': maxRequests.toString(),
'X-RateLimit-Remaining': (maxRequests - clientData.count).toString(),
'X-RateLimit-Reset': clientData.resetTime.toString(),
}
};
};
};
Using Middleware
Direct Usage
Use middleware directly with createLoader
:
import { createLoader } from 'reactr-middleware';
export const loader = createLoader([
loggerMiddleware,
authMiddleware('/login'),
rateLimitMiddleware(50, 60000)
]);
Registry Usage
Register middleware for reuse across routes:
// middleware.config.ts
import { registerMiddleware } from 'reactr-middleware';
registerMiddleware('protected', [
loggerMiddleware,
authMiddleware('/login'),
rateLimitMiddleware(50, 60000)
]);
// route file
export const loader = createLoaderFromRegistry('protected');
Execution Modes
Sequential Execution (Default)
Middleware execute one after another, with data flowing between them:
export const loader = createLoader([
middleware1, // Executes first
middleware2, // Executes second, receives data from middleware1
middleware3 // Executes third, receives combined data
]);
Parallel Execution
Independent middleware can run simultaneously for better performance:
export const loader = createLoader([
corsMiddleware, // Runs in parallel
rateLimitMiddleware, // Runs in parallel
loggerMiddleware // Runs in parallel
], { parallel: true });
WARNING
Use parallel execution only when middleware don't depend on each other's results.
Data Flow
Passing Data
Middleware can pass data to subsequent middleware and your component:
const userMiddleware: Middleware = async (context) => {
const user = await getCurrentUser(context);
return {
continue: true,
data: { user, roles: user.roles }
};
};
const permissionMiddleware: Middleware = async (context) => {
// Access data from previous middleware
const { user, roles } = context.data || {};
if (!roles.includes('admin')) {
return { continue: false, redirect: '/unauthorized' };
}
return {
continue: true,
data: { permissions: ['read', 'write', 'delete'] }
};
};
Accessing Data in Components
export default function AdminPanel() {
const { middlewareData } = useLoaderData() as { middlewareData: any };
const { user, permissions } = middlewareData;
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Permissions: {permissions.join(', ')}</p>
</div>
);
}
Error Handling
Graceful Failures
const apiMiddleware: Middleware = async (context) => {
try {
const data = await fetchApiData();
return { continue: true, data };
} catch (error) {
// Log error but continue
console.error('API failed:', error);
return { continue: true, data: { apiError: true } };
}
};
Hard Failures
const criticalMiddleware: Middleware = async (context) => {
try {
await criticalOperation();
return { continue: true };
} catch (error) {
// Stop execution
return {
continue: false,
redirect: '/error',
data: { error: error.message }
};
}
};
Loader-Level Error Handling
export const loader = createLoader([middleware1, middleware2], {
rejectOnError: true, // Throw on middleware errors
redirect: '/error' // Redirect on errors instead of throwing
});
Best Practices
1. Keep Middleware Focused
Each middleware should have a single responsibility:
// ✅ Good - focused on one concern
const authMiddleware = () => { /* only auth logic */ };
const logMiddleware = () => { /* only logging logic */ };
// ❌ Bad - multiple concerns
const authAndLogMiddleware = () => { /* auth + logging */ };
2. Make Middleware Configurable
// ✅ Good - configurable
const cacheMiddleware = (ttl: number = 300) => {
return async (context) => {
// Use ttl parameter
};
};
// ❌ Bad - hardcoded values
const cacheMiddleware = async (context) => {
const ttl = 300; // Hardcoded
};
3. Use TypeScript
interface AuthData {
user: User;
permissions: string[];
}
const authMiddleware: Middleware = async (context) => {
const authData: AuthData = await authenticate(context);
return { continue: true, data: authData };
};
4. Test Middleware Independently
// middleware.test.ts
import { authMiddleware } from './auth';
describe('authMiddleware', () => {
it('should allow valid tokens', async () => {
const context = createMockContext({
headers: { Authorization: 'Bearer valid-token' }
});
const result = await authMiddleware(context);
expect(result.continue).toBe(true);
});
});
Common Patterns
Conditional Middleware
const conditionalMiddleware = (condition: boolean, middleware: Middleware): Middleware => {
return async (context) => {
if (!condition) {
return { continue: true };
}
return middleware(context);
};
};
// Usage
registerMiddleware('development', [
conditionalMiddleware(process.env.NODE_ENV === 'development', debugMiddleware)
]);
Middleware Composition
const combineMiddleware = (...middlewares: Middleware[]): Middleware => {
return async (context) => {
// Execute all middleware and combine results
const results = await Promise.all(
middlewares.map(middleware => middleware(context))
);
// Custom combination logic
return {
continue: results.every(r => r.continue),
data: Object.assign({}, ...results.map(r => r.data))
};
};
};
TIP
Start with built-in middleware and gradually create custom ones as your needs grow. Most applications can get by with just the common middleware patterns.