What is Middleware?
Middleware functions run code before and/or after route actions. They’re a powerful way to add functionality like logging, authentication, error handling, and data parsing to your routes.
import type { Middleware } from 'remix/fetch-router'
function logger () : Middleware {
return async ( context , next ) => {
let start = Date . now ()
// Call next() to invoke the next middleware or action
let response = await next ()
let duration = Date . now () - start
console . log ( ` ${ context . method } ${ context . url . pathname } - ${ response . status } ( ${ duration } ms)` )
return response
}
}
Middleware Signature
The Middleware type defines the function signature:
interface Middleware <
method extends RequestMethod | 'ANY' = RequestMethod | 'ANY' ,
params extends Record < string , any > = {},
> {
(
context : RequestContext < params >,
next : NextFunction ,
) : Response | undefined | void | Promise < Response | undefined | void >
}
type NextFunction = () => Promise < Response >
context
RequestContext<params>
required
The request context object containing request, url, params, method, headers, and context storage methods (get, set, has).
A function that invokes the next middleware or handler in the chain. Returns a Promise that resolves to the Response.
return
Response | undefined | void | Promise<...>
Middleware can:
Return a Response to short-circuit the chain
Call next() and return its result
Return nothing (implicitly calls next())
Global Middleware
Global middleware runs on every request before routes are matched:
import { createRouter } from 'remix/fetch-router'
import { logger } from 'remix/logger-middleware'
import { formData } from 'remix/form-data-middleware'
let router = createRouter ({
middleware: [
logger (),
formData (),
],
})
Global middleware is useful for:
Request logging
Parsing request bodies
Session management
Security headers
Compression
Static file serving
Keep global middleware lightweight since it runs on every request.
Route Middleware
Route middleware runs only for specific routes, after global middleware but before the action:
router . map ( routes . admin . dashboard , {
middleware: [ requireAuth ()],
action () {
return new Response ( 'Admin Dashboard' )
},
})
Inline Middleware
You can also attach middleware directly to an action:
router . map ( routes , {
actions: {
home () {
return new Response ( 'Home' )
},
admin: {
actions: {
dashboard: {
middleware: [ requireAuth ()],
action () {
return new Response ( 'Dashboard' )
},
},
},
},
},
})
Controller Middleware
Apply middleware to all routes in a controller:
router . map ( routes . admin , {
// Runs on all admin routes
middleware: [ requireAuth ()],
actions: {
dashboard () {
return new Response ( 'Dashboard' )
},
users () {
return new Response ( 'Users' )
},
settings: {
// Additional middleware just for settings
middleware: [ requireSuperAdmin ()],
action () {
return new Response ( 'Settings' )
},
},
},
})
Middleware cascades: [requireAuth(), requireSuperAdmin()] runs for the settings route.
Writing Middleware
Basic Pattern
Middleware is typically a factory function that returns the middleware function:
function myMiddleware ( options ?: Options ) : Middleware {
return async ( context , next ) => {
// Before action
console . log ( 'Before' )
// Call next middleware/action
let response = await next ()
// After action
console . log ( 'After' )
return response
}
}
Short-Circuiting
Return a Response to skip downstream middleware and actions:
function requireAuth () : Middleware {
return ( context , next ) => {
let token = context . headers . get ( 'Authorization' )
if ( ! token ) {
// Short-circuit: don't call next()
return new Response ( 'Unauthorized' , { status: 401 })
}
// Continue to next middleware/action
return next ()
}
}
Modifying Context
Use context.set() to store data for downstream middleware and actions:
import { createContextKey } from 'remix/fetch-router'
import { Session } from 'remix/session'
function auth ( options ?: AuthOptions ) : Middleware {
let token = options ?. token ?? 'secret'
return async ( context , next ) => {
let authHeader = context . headers . get ( 'Authorization' )
if ( authHeader !== `Bearer ${ token } ` ) {
return new Response ( 'Unauthorized' , { status: 401 })
}
// Store authenticated user in context
let userKey = createContextKey < User >()
context . set ( userKey , { id: '123' , name: 'Alice' })
return next ()
}
}
// Access in actions
router . get ( '/profile' , ({ get }) => {
let user = get ( userKey )
return Response . json ({ user })
})
Modifying Responses
Modify the response returned from downstream:
function addSecurityHeaders () : Middleware {
return async ( context , next ) => {
let response = await next ()
// Clone to modify headers
response = new Response ( response . body , response )
response . headers . set ( 'X-Content-Type-Options' , 'nosniff' )
response . headers . set ( 'X-Frame-Options' , 'DENY' )
response . headers . set ( 'X-XSS-Protection' , '1; mode=block' )
return response
}
}
Error Handling
Wrap next() in try/catch to handle errors:
function errorHandler () : Middleware {
return async ( context , next ) => {
try {
return await next ()
} catch ( error ) {
console . error ( 'Error:' , error )
return new Response ( 'Internal Server Error' , { status: 500 })
}
}
}
Common Middleware Examples
Authentication
import { Session } from 'remix/session'
function requireAuth () : Middleware {
return ({ get }, next ) => {
let session = get ( Session )
let username = session . get ( 'username' )
if ( ! username ) {
return new Response ( 'Unauthorized' , { status: 401 })
}
return next ()
}
}
Request Logging
import { logger } from 'remix/logger-middleware'
// Simple logger
let router = createRouter ({
middleware: [ logger ()],
})
// Custom logger
function customLogger () : Middleware {
return async ( context , next ) => {
let start = Date . now ()
console . log ( `--> ${ context . method } ${ context . url . pathname } ` )
let response = await next ()
let duration = Date . now () - start
console . log ( `<-- ${ response . status } ( ${ duration } ms)` )
return response
}
}
import { formData } from 'remix/form-data-middleware'
let router = createRouter ({
middleware: [ formData ()],
})
router . post ( '/contact' , ({ get }) => {
let formData = get ( FormData )
let message = formData . get ( 'message' )
return new Response ( `Got: ${ message } ` )
})
Session Management
import { session } from 'remix/session-middleware'
import { createCookie } from 'remix/cookie'
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
import { Session } from 'remix/session'
let sessionCookie = createCookie ( '__session' , {
secrets: [ 's3cr3t' ],
})
let sessionStorage = createCookieSessionStorage ()
let router = createRouter ({
middleware: [ session ( sessionCookie , sessionStorage )],
})
router . post ( '/login' , ({ get }) => {
let session = get ( Session )
let formData = get ( FormData )
let username = formData . get ( 'username' )
session . set ( 'username' , username )
return new Response ( 'Logged in' )
})
function cors ( options ?: CorsOptions ) : Middleware {
let origin = options ?. origin ?? '*'
let methods = options ?. methods ?? 'GET,POST,PUT,DELETE'
return async ( context , next ) => {
// Handle preflight
if ( context . method === 'OPTIONS' ) {
return new Response ( null , {
status: 204 ,
headers: {
'Access-Control-Allow-Origin' : origin ,
'Access-Control-Allow-Methods' : methods ,
'Access-Control-Allow-Headers' : 'Content-Type' ,
},
})
}
let response = await next ()
// Add CORS headers to response
response = new Response ( response . body , response )
response . headers . set ( 'Access-Control-Allow-Origin' , origin )
return response
}
}
Rate Limiting
function rateLimit ( options : RateLimitOptions ) : Middleware {
let requests = new Map < string , number []>()
let { maxRequests = 100 , windowMs = 60000 } = options
return ( context , next ) => {
let ip = context . headers . get ( 'X-Forwarded-For' ) ?? 'unknown'
let now = Date . now ()
let timestamps = requests . get ( ip ) ?? []
// Remove old timestamps
timestamps = timestamps . filter ( t => now - t < windowMs )
if ( timestamps . length >= maxRequests ) {
return new Response ( 'Too Many Requests' , { status: 429 })
}
timestamps . push ( now )
requests . set ( ip , timestamps )
return next ()
}
}
Middleware Execution Order
Middleware runs in this order:
Global middleware (defined in createRouter)
Controller middleware (defined in parent controller)
Route middleware (defined in action’s middleware array)
Action (the actual route handler)
let router = createRouter ({
middleware: [ a ()], // 1. Runs first
})
router . map ( routes . admin , {
middleware: [ b ()], // 2. Runs second for all admin routes
actions: {
dashboard: {
middleware: [ c ()], // 3. Runs third for dashboard only
action () {
// 4. Finally, the action runs
return new Response ( 'Dashboard' )
},
},
},
})
Each middleware can call next() to continue down the chain, or return a Response to short-circuit.
Async Middleware
Middleware functions can be async:
function fetchUser () : Middleware {
return async ( context , next ) => {
let userId = context . headers . get ( 'X-User-ID' )
// Async operation
let user = await db . users . findById ( userId )
let userKey = createContextKey < User >()
context . set ( userKey , user )
return next ()
}
}
Testing Middleware
Test middleware in isolation:
import { describe , it } from 'node:test'
import { RequestContext } from 'remix/fetch-router'
describe ( 'auth middleware' , () => {
it ( 'blocks unauthorized requests' , async () => {
let middleware = requireAuth ()
let request = new Request ( 'https://example.com/admin' )
let context = new RequestContext ( request )
let next = async () => new Response ( 'Protected' )
let response = await middleware ( context , next )
assert . equal ( response ?. status , 401 )
})
it ( 'allows authorized requests' , async () => {
let middleware = requireAuth ()
let request = new Request ( 'https://example.com/admin' , {
headers: { Authorization: 'Bearer secret' },
})
let context = new RequestContext ( request )
let next = async () => new Response ( 'Protected' )
let response = await middleware ( context , next )
assert . equal ( response ?. status , 200 )
})
})
Next Steps
Controllers Organize routes with nested controllers
Forms Handle form submissions with middleware