Skip to main content
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,
    }),
  ],
})

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.)

Middleware Guide

Learn about middleware concepts

Built-in Middleware

Explore included middleware

Build docs developers (and LLMs) love