Middleware Signature
All middleware follows this signature:import type { Middleware, RequestContext } from 'remix/fetch-router'
function myMiddleware(): Middleware {
return async (context: RequestContext, next: () => Promise<Response>): Promise<Response> => {
// Before action
let response = await next()
// After action
return response
}
}
Basic Middleware Example
Create a simple timing middleware:function timing(): Middleware {
return async (context, next) => {
let start = Date.now()
let response = await next()
let duration = Date.now() - start
response.headers.set('X-Response-Time', `${duration}ms`)
return response
}
}
// Usage
let router = createRouter({
middleware: [timing()],
})
Middleware with Configuration
Create configurable middleware:interface RateLimitOptions {
windowMs: number
max: number
message?: string
}
function rateLimit(options: RateLimitOptions): Middleware {
let requests = new Map<string, number[]>()
return async (context, next) => {
let ip = context.headers.get('x-forwarded-for') || 'unknown'
let now = Date.now()
let windowStart = now - options.windowMs
// Get recent requests from this IP
let userRequests = requests.get(ip) || []
userRequests = userRequests.filter((time) => time > windowStart)
if (userRequests.length >= options.max) {
return new Response(
options.message || 'Too many requests',
{ status: 429 }
)
}
userRequests.push(now)
requests.set(ip, userRequests)
return next()
}
}
// Usage
let router = createRouter({
middleware: [
rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Rate limit exceeded',
}),
],
})
Authentication Middleware
Create middleware for authentication:import { createContextKey } from 'remix/fetch-router'
import type { User } from './types'
let UserKey = createContextKey<User>()
function authenticate(options?: { required?: boolean }): Middleware {
return async (context, next) => {
let token = context.headers.get('Authorization')?.replace('Bearer ', '')
if (!token) {
if (options?.required) {
return Response.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
return next()
}
try {
let user = await verifyToken(token)
context.set(UserKey, user)
return next()
} catch (error) {
return Response.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
}
}
// Usage
router.get(routes.profile, {
middleware: [authenticate({ required: true })],
action({ get }) {
let user = get(UserKey)
return Response.json({ user })
},
})
Request Validation
Validate request data:import { parse, object, string } from 'remix/data-schema'
function validateBody<T>(schema: any): Middleware {
return async (context, next) => {
if (
context.method === 'POST' ||
context.method === 'PUT' ||
context.method === 'PATCH'
) {
try {
let data = await context.request.json()
let validated = parse(schema, data)
// Store validated data in context
let ValidatedDataKey = createContextKey<T>()
context.set(ValidatedDataKey, validated)
} catch (error) {
return Response.json(
{ error: 'Validation failed', details: error.issues },
{ status: 400 }
)
}
}
return next()
}
}
// Usage
let createUserSchema = object({
name: string().minLength(2),
email: string().email(),
})
router.post(routes.users, {
middleware: [validateBody(createUserSchema)],
action({ get }) {
let data = get(ValidatedDataKey)
// data is fully validated
},
})
CORS Middleware
Handle cross-origin requests:interface CorsOptions {
origin?: string | string[]
methods?: string[]
allowedHeaders?: string[]
exposedHeaders?: string[]
credentials?: boolean
maxAge?: number
}
function cors(options: CorsOptions = {}): Middleware {
return async (context, next) => {
let origin = context.headers.get('Origin')
// Handle preflight
if (context.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': options.origin || '*',
'Access-Control-Allow-Methods': (options.methods || ['GET', 'POST', 'PUT', 'DELETE']).join(', '),
'Access-Control-Allow-Headers': (options.allowedHeaders || ['Content-Type', 'Authorization']).join(', '),
'Access-Control-Max-Age': String(options.maxAge || 86400),
},
})
}
let response = await next()
// Add CORS headers to response
response.headers.set('Access-Control-Allow-Origin', options.origin || '*')
if (options.credentials) {
response.headers.set('Access-Control-Allow-Credentials', 'true')
}
if (options.exposedHeaders) {
response.headers.set(
'Access-Control-Expose-Headers',
options.exposedHeaders.join(', ')
)
}
return response
}
}
// Usage
let router = createRouter({
middleware: [
cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
}),
],
})
Response Transformation
Transform responses:function wrapResponse(): Middleware {
return async (context, next) => {
let response = await next()
// Only wrap JSON responses
if (response.headers.get('Content-Type')?.includes('application/json')) {
let data = await response.json()
return Response.json({
success: response.ok,
status: response.status,
data: response.ok ? data : null,
error: response.ok ? null : data,
timestamp: new Date().toISOString(),
}, {
status: response.status,
headers: response.headers,
})
}
return response
}
}
Error Handling Middleware
Catch and handle errors:function errorHandler(): Middleware {
return async (context, next) => {
try {
return await next()
} catch (error) {
console.error('Request error:', error)
if (error instanceof ValidationError) {
return Response.json(
{ error: error.message, fields: error.fields },
{ status: 400 }
)
}
if (error instanceof NotFoundError) {
return Response.json(
{ error: 'Resource not found' },
{ status: 404 }
)
}
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
}
Middleware Composition
Compose multiple middleware:function compose(...middlewares: Middleware[]): Middleware {
return async (context, next) => {
let index = -1
async function dispatch(i: number): Promise<Response> {
if (i <= index) {
throw new Error('next() called multiple times')
}
index = i
let middleware = middlewares[i]
if (!middleware) {
return next()
}
return middleware(context, () => dispatch(i + 1))
}
return dispatch(0)
}
}
// Usage
let authStack = compose(
authenticate(),
rateLimit({ windowMs: 60000, max: 100 }),
validateBody(schema)
)
router.post(routes.users, {
middleware: [authStack],
action() {
// All middleware have run
},
})
Testing Middleware
Test middleware in isolation:import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
describe('timing middleware', () => {
it('adds response time header', async () => {
let middleware = timing()
let response = await middleware(
{ headers: new Headers() } as any,
async () => new Response('OK')
)
assert.ok(response.headers.has('X-Response-Time'))
assert.ok(response.headers.get('X-Response-Time')?.endsWith('ms'))
})
})
Best Practices
- Keep middleware focused on a single responsibility
- Make middleware configurable with options
- Always call
next()unless short-circuiting - Use context to pass data between middleware
- Handle errors gracefully
- Document middleware behavior
- Test middleware independently
- Order middleware carefully (auth before rate limiting, etc.)
Related Documentation
Middleware Guide
Learn about middleware concepts
Built-in Middleware
Explore included middleware