Documentation Index Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt
Use this file to discover all available pages before exploring further.
Middleware in Remix allows you to run code before and after route actions, enabling powerful patterns for authentication, logging, data transformation, and more.
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 ,
}),
],
})
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.)
Middleware Guide Learn about middleware concepts
Built-in Middleware Explore included middleware