Middleware
Middleware is a function that runs before (and optionally after) your route handler. Middleware is how you add cross-cutting behavior to your application --- logging, authentication, compression, database access, and more --- without repeating yourself in every handler.
How Middleware Works
A middleware function receives two arguments:
context--- The request context object, which contains the request, URL, params, and a key-value store for passing data between middleware and handlers.next--- A function that calls the next middleware in the chain, or the route handler if there are no more middleware.
async function myMiddleware(context, next) {
// Code here runs BEFORE the handler
console.log('Request:', context.request.method, context.url.pathname)
let response = await next()
// Code here runs AFTER the handler
console.log('Response:', response.status)
return response
}The middleware function can:
- Return nothing --- Call
next()and let the chain continue. - Return a
Response--- Short-circuit the chain and skip the handler entirely. - Modify the context --- Add data that downstream middleware and the handler can access.
- Modify the response --- Intercept the response from
next()and change it before returning.
The Middleware Function Signature
type Middleware = (
context: RequestContext,
next: () => Promise<Response>,
) => Response | Promise<Response> | void | Promise<void>If the middleware returns void (or undefined), Remix calls next() automatically. This means simple middleware that only adds data to the context can skip calling next():
// This middleware adds data to the context and implicitly calls next()
function addRequestId(context, next) {
context.set(RequestId, crypto.randomUUID())
// No return, no next() call --- Remix handles it
}Returning vs. not returning
If you need to modify the response, you must call next() yourself and return the result:
async function addHeaders(context, next) {
let response = await next()
response.headers.set('X-Custom', 'value')
return response
}If you just need to add data to the context, you can omit the next() call --- Remix will call it for you.
Global Middleware
Global middleware runs on every request. You add it when creating the router:
import { createRouter } from 'remix/fetch-router'
import { compression } from 'remix/compression-middleware'
import { logger } from 'remix/logger-middleware'
import { cors } from 'remix/cors-middleware'
let router = createRouter({
middleware: [
logger(),
cors({ origin: 'https://example.com' }),
compression(),
],
})Global middleware runs in the order you provide it. In this example, logger() runs first, then cors(), then compression(), and finally the route handler.
Per-Route Middleware
You can also attach middleware to specific routes or route groups when calling router.map():
import { requireAuth } from './middleware/auth.ts'
import { rateLimit } from './middleware/rate-limit.ts'
// Only this route group requires auth
router.map(routes.admin, {
middleware: [requireAuth],
actions: {
index() { /* ... */ },
// ...
},
})
// Rate limiting on the API
router.map(routes.api.webhooks, {
middleware: [rateLimit({ max: 100, windowMs: 60_000 })],
handler({ request }) { /* ... */ },
})Per-route middleware runs after global middleware and before the handler.
Middleware Ordering Matters
The order of middleware is significant. Each middleware in the chain can:
- Access data set by earlier middleware
- Modify the request before later middleware sees it
- Wrap the response from later middleware
A common ordering:
let router = createRouter({
middleware: [
logger(), // 1. Log every request
compression(), // 2. Compress responses
staticFiles('./public'), // 3. Serve static files (skips remaining middleware for static files)
csrf(), // 4. CSRF protection
formData(), // 5. Parse form data
methodOverride(), // 6. Support PUT/DELETE from forms (reads form data)
session(cookie, storage), // 7. Load session
auth({ schemes }), // 8. Load auth state from session
],
})formData before methodOverride
methodOverride() reads the _method field from the parsed form data, so it must come after formData(). If you reverse the order, method override will not work.
Type-Safe Context with createContextKey()
Middleware adds data to the request context. To do this in a type-safe way, you create context keys:
import { createContextKey } from 'remix/fetch-router'
// Create a typed context key
export const RequestId = createContextKey<string>('RequestId')
export const CurrentUser = createContextKey<User | null>('CurrentUser')Middleware sets values using context.set():
function addRequestId(context, next) {
context.set(RequestId, crypto.randomUUID())
}Handlers (and other middleware) retrieve values using context.get():
handler({ get }) {
let requestId = get(RequestId) // string
let user = get(CurrentUser) // User | null
}TypeScript enforces that the value matches the key's type. You cannot accidentally set a number for a string key or read the wrong type.
Built-In Context Keys
Remix provides several built-in context keys used by its middleware:
| Key | Package | Set By | Type |
|---|---|---|---|
Database | remix/data-table | Database middleware | Database |
Session | remix/session | Session middleware | Session |
Auth | remix/auth-middleware | Auth middleware | AuthState |
FormData | remix/form-data-middleware | Form data middleware | FormData |
Built-In Middleware Overview
Remix ships with a rich set of middleware. Each one is a separate import, and you only include the ones you need.
compression()
import { compression } from 'remix/compression-middleware'Compresses response bodies using gzip or brotli based on the client's Accept-Encoding header. Automatically skips compression for small responses and already-compressed content types.
logger()
import { logger } from 'remix/logger-middleware'Logs each request and response, including method, URL, status code, and response time. Useful during development.
cors()
import { cors } from 'remix/cors-middleware'
cors({
origin: 'https://example.com', // or ['https://a.com', 'https://b.com']
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type'],
credentials: true,
})Handles Cross-Origin Resource Sharing (CORS) by setting the appropriate response headers. Required when your API is accessed from a different domain than the one serving it.
csrf()
import { csrf } from 'remix/csrf-middleware'Protects against Cross-Site Request Forgery attacks by verifying that form submissions originate from your site. Works with the session middleware.
staticFiles()
import { staticFiles } from 'remix/static-middleware'
staticFiles('./public')Serves static files from a directory. Supports ETag-based caching and range requests. Requests that match a static file are handled immediately without running subsequent middleware.
session()
import { session } from 'remix/session-middleware'
session(cookie, storage)Loads and saves session data. Makes context.get(Session) available to handlers. See the Sessions & Cookies guide.
formData()
import { formData } from 'remix/form-data-middleware'Parses application/x-www-form-urlencoded and multipart/form-data request bodies. Makes context.get(FormData) available to handlers. See the Forms & Mutations guide.
methodOverride()
import { methodOverride } from 'remix/method-override-middleware'Reads a _method hidden field from form data and changes the request method accordingly. Enables HTML forms to submit PUT and DELETE requests.
asyncContext()
import { asyncContext } from 'remix/async-context-middleware'Enables access to the request context from anywhere in the call stack using Node.js AsyncLocalStorage, without explicitly passing it through function arguments.
auth()
import { auth, requireAuth } from 'remix/auth-middleware'Loads authentication state from configured auth schemes. requireAuth() creates a middleware that redirects unauthenticated users. See the Sessions & Cookies guide for more.
cop()
import { cop } from 'remix/cop-middleware'Content Security Policy middleware. Sets Content-Security-Policy headers to control which resources the browser is allowed to load.
Writing Custom Middleware
Creating your own middleware is straightforward. Here are several common patterns.
Adding Data to Context
import { createContextKey } from 'remix/fetch-router'
import type { Middleware } from 'remix/fetch-router'
// 1. Create a context key
export const Timing = createContextKey<{ start: number }>('Timing')
// 2. Write the middleware
export function timing(): Middleware {
return (context, next) => {
context.set(Timing, { start: Date.now() })
}
}
// 3. Use in handlers
handler({ get }) {
let timing = get(Timing)
console.log(`Request started at ${timing.start}`)
}Short-Circuiting the Chain
Middleware can return a Response to skip the rest of the chain:
export function maintenance(): Middleware {
return (context, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
return new Response('Site is under maintenance', {
status: 503,
headers: { 'Retry-After': '3600' },
})
}
return next()
}
}Wrapping the Response
Middleware can modify the response before it is sent to the client:
export function serverTiming(): Middleware {
return async (context, next) => {
let start = performance.now()
let response = await next()
let duration = performance.now() - start
response.headers.set(
'Server-Timing',
`total;dur=${duration.toFixed(1)}`,
)
return response
}
}Error Handling
Middleware can catch errors from downstream middleware and handlers:
export function errorBoundary(): Middleware {
return async (context, next) => {
try {
return await next()
} catch (error) {
console.error('Unhandled error:', error)
return new Response('Internal Server Error', { status: 500 })
}
}
}Middleware Composition Patterns
Conditional Middleware
Run middleware only in certain environments:
let middleware = []
if (process.env.NODE_ENV === 'development') {
middleware.push(logger())
}
middleware.push(compression())
middleware.push(staticFiles('./public'))
let router = createRouter({ middleware })Middleware Factories
Create middleware that accepts configuration:
export function rateLimit(options: { max: number; windowMs: number }): Middleware {
let requests = new Map<string, { count: number; resetAt: number }>()
return (context, next) => {
let ip = context.request.headers.get('x-forwarded-for') ?? 'unknown'
let now = Date.now()
let record = requests.get(ip)
if (!record || now > record.resetAt) {
record = { count: 0, resetAt: now + options.windowMs }
requests.set(ip, record)
}
record.count++
if (record.count > options.max) {
return new Response('Too Many Requests', {
status: 429,
headers: { 'Retry-After': String(Math.ceil((record.resetAt - now) / 1000)) },
})
}
return next()
}
}Combining Multiple Middleware
Group related middleware into a single function:
import { session } from 'remix/session-middleware'
import { auth } from 'remix/auth-middleware'
import { formData } from 'remix/form-data-middleware'
export function webMiddleware(cookie, storage, authSchemes) {
return [
formData(),
methodOverride(),
session(cookie, storage),
auth({ schemes: authSchemes }),
]
}
// Usage
let router = createRouter({
middleware: [
logger(),
compression(),
staticFiles('./public'),
...webMiddleware(sessionCookie, sessionStorage, [sessionAuth]),
],
})Role-Based Access Control
Build middleware that checks for specific roles:
import { Auth } from 'remix/auth-middleware'
export function requireRole(role: string): Middleware {
return (context, next) => {
let authState = context.get(Auth)
if (!authState.ok) {
return new Response(null, {
status: 302,
headers: { Location: '/login' },
})
}
if (authState.identity.role !== role) {
return new Response('Forbidden', { status: 403 })
}
return next()
}
}
// Usage
router.map(routes.admin, {
middleware: [requireRole('admin')],
actions: { /* ... */ },
})Middleware Type Safety
When middleware adds data to the context, you can encode this in the type system so that handlers know what data is available:
import type { Middleware } from 'remix/fetch-router'
import { createContextKey } from 'remix/fetch-router'
import { Database } from 'remix/data-table'
type SetDatabaseContextTransform = readonly [
readonly [typeof Database, Database],
]
export function loadDatabase(): Middleware<
'ANY',
{},
SetDatabaseContextTransform
> {
return async (context, next) => {
context.set(Database, db)
return next()
}
}The third type parameter (SetDatabaseContextTransform) declares what the middleware adds to the context. This enables TypeScript to verify that handlers calling get(Database) are downstream of this middleware.
Related
- Routing In Depth --- How routes and middleware connect.
- Request & Response --- The objects middleware works with.
- Sessions & Cookies --- Session middleware in detail.
- Error Handling --- Using middleware for error boundaries.