Skip to content

Middleware ​

Middleware lets you run code before a request is handled by your route. Use it for authentication, logging, rate limiting, headers manipulation, and other cross-cutting concerns.

Basic Middleware ​

Middleware is a function that receives a request, the app context, and a next function:

tsx
import type { MiddlewareHandler } from '@ereo/core'

export const middleware: MiddlewareHandler = async (request, context, next) => {
  console.log(`${request.method} ${request.url}`)

  // Call next() to continue to the route handler
  const response = await next()

  console.log(`Response: ${response.status}`)
  return response
}

Signature: The middleware signature is always (request, context, next). The context parameter is the AppContext object — use it to share data between middleware and loaders (e.g., context.set('user', user)).

Route-Level Middleware ​

Add middleware to specific routes with a _middleware.ts file:

routes/
├── _middleware.ts        # Applies to all routes
├── index.tsx
├── dashboard/
│   ├── _middleware.ts    # Applies to /dashboard/*
│   └── index.tsx
└── api/
    ├── _middleware.ts    # Applies to /api/*
    └── users.ts
tsx
// routes/dashboard/_middleware.ts
import type { MiddlewareHandler } from '@ereo/core'

export const middleware: MiddlewareHandler = async (request, context, next) => {
  const user = await getUser(request)

  if (!user) {
    return Response.redirect('/login')
  }

  context.set('user', user)
  return next()
}

Named Middleware ​

Register middleware globally and reference by name:

tsx
// src/middleware/index.ts
import { registerMiddleware } from '@ereo/router'

registerMiddleware('auth', async (request, context, next) => {
  const user = await getUser(request)
  if (!user) return Response.redirect('/login')
  context.set('user', user)
  return next()
})

registerMiddleware('admin', async (request, context, next) => {
  const user = context.get('user')
  if (!user?.isAdmin) return new Response('Forbidden', { status: 403 })
  return next()
})

registerMiddleware('logger', async (request, context, next) => {
  const start = Date.now()
  const response = await next()
  console.log(`${request.method} ${request.url} - ${Date.now() - start}ms`)
  return response
})

Use in route config:

tsx
// routes/admin/index.tsx
export const config = {
  middleware: ['auth', 'admin', 'logger']
}

export default function AdminDashboard() {
  return <h1>Admin Dashboard</h1>
}

Composing Middleware ​

Combine multiple middleware handlers:

tsx
import { composeMiddleware } from '@ereo/router'

const withAuth = async (request, context, next) => {
  const user = await getUser(request)
  if (!user) return Response.redirect('/login')
  context.set('user', user)
  return next()
}

const withLogging = async (request, context, next) => {
  console.log(`${request.method} ${request.url}`)
  return next()
}

// Combine into a single middleware
export const middleware = composeMiddleware(withLogging, withAuth)

Conditional Middleware ​

Apply middleware based on conditions:

tsx
import { when, method, path } from '@ereo/router'

// Only run for POST requests
export const middleware = method('POST', async (request, context, next) => {
  // Validate CSRF token
  const token = request.headers.get('X-CSRF-Token')
  if (!validateCsrf(token)) {
    return new Response('Invalid CSRF token', { status: 403 })
  }
  return next()
})

// Only run for specific paths
export const apiMiddleware = path('/api/*', async (request, context, next) => {
  // Add CORS headers
  const response = await next()
  response.headers.set('Access-Control-Allow-Origin', '*')
  return response
})

// Conditional based on request
export const conditionalMiddleware = when(
  (request) => request.headers.get('X-Api-Key') !== null,
  async (request, context, next) => {
    // API key authentication
    return next()
  }
)

Built-In Middleware ​

EreoJS provides common middleware out of the box:

CORS ​

tsx
import { createCorsMiddleware } from '@ereo/router'

export const middleware = createCorsMiddleware({
  origin: ['https://example.com', 'https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
})

Rate Limiting ​

tsx
import { createRateLimitMiddleware } from '@ereo/router'

export const middleware = createRateLimitMiddleware({
  windowMs: 60 * 1000,  // 1 minute
  max: 100,             // 100 requests per minute
  keyGenerator: (request) => {
    // Rate limit by IP or user
    return request.headers.get('X-Forwarded-For') || 'anonymous'
  },
  handler: (request) => {
    return new Response('Too many requests', { status: 429 })
  }
})

Logging ​

tsx
import { createLoggerMiddleware } from '@ereo/router'

export const middleware = createLoggerMiddleware({
  format: ':method :url :status :response-time ms',
  skip: (request) => request.url.includes('/health')
})

Middleware Chain Execution ​

Middleware executes in order, wrapping around the route handler:

Request
    │
    ▼
┌─────────────┐
│ Middleware 1│ ─┐
└─────────────┘  │
    │            │
    ▼            │
┌─────────────┐  │
│ Middleware 2│ ─┤
└─────────────┘  │
    │            │
    ▼            │
┌─────────────┐  │
│ Route       │  │
│ Handler     │  │
└─────────────┘  │
    │            │
    ▼            │
  Response ◄─────┘
tsx
// Middleware 1
const first: MiddlewareHandler = async (request, context, next) => {
  console.log('1. Before')
  const response = await next()
  console.log('4. After')
  return response
}

// Middleware 2
const second: MiddlewareHandler = async (request, context, next) => {
  console.log('2. Before')
  const response = await next()
  console.log('3. After')
  return response
}

// Output:
// 1. Before
// 2. Before
// [Route Handler]
// 3. After
// 4. After

Passing Data to Routes ​

Use the request context to pass data from middleware to routes:

tsx
// Middleware
export const middleware: MiddlewareHandler = async (request, context, next) => {
  const user = await getUser(request)
  context.set('user', user)
  return next()
}

// Route
export const loader = createLoader(async ({ context }) => {
  const user = context.get('user')
  return { user }
})

Modifying Responses ​

Middleware can modify responses:

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  const response = await next()

  // Add security headers
  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
}

Short-Circuiting ​

Return a response directly to skip remaining middleware and the route:

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  // Maintenance mode
  if (process.env.MAINTENANCE_MODE === 'true') {
    return new Response('Site under maintenance', {
      status: 503,
      headers: { 'Retry-After': '3600' }
    })
  }

  return next()
}

Error Handling ​

Handle errors in middleware:

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  try {
    return await next()
  } catch (error) {
    console.error('Route error:', error)

    if (error instanceof Response) {
      return error
    }

    return new Response('Internal Server Error', { status: 500 })
  }
}

Type-Safe Middleware ​

Use typed middleware for better type safety:

tsx
import { createMiddleware } from '@ereo/router'

interface AuthContext {
  user: { id: string; email: string }
}

export const authMiddleware = createMiddleware<AuthContext>(
  async (request, context, next) => {
    const user = await getUser(request)
    if (!user) return Response.redirect('/login')

    context.set('user', user)
    return next()
  }
)

Server Middleware ​

Apply middleware at the server level for all routes:

tsx
// src/index.ts
import { createApp } from '@ereo/core'
import { createServer, logger, cors, securityHeaders } from '@ereo/server'

const app = createApp()

app.middleware(logger())
app.middleware(cors({ origin: '*' }))
app.middleware(securityHeaders())

const server = createServer(app)

Common Patterns ​

Authentication ​

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '')

  if (!token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const user = await verifyToken(token)
    context.set('user', user)
    return next()
  } catch {
    return Response.json({ error: 'Invalid token' }, { status: 401 })
  }
}

Request Validation ​

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  if (request.method === 'POST') {
    const contentType = request.headers.get('Content-Type')

    if (!contentType?.includes('application/json')) {
      return Response.json(
        { error: 'Content-Type must be application/json' },
        { status: 415 }
      )
    }
  }

  return next()
}

Caching ​

tsx
export const middleware: MiddlewareHandler = async (request, context, next) => {
  // Only cache GET requests
  if (request.method !== 'GET') {
    return next()
  }

  const cacheKey = request.url
  const cached = await cache.get(cacheKey)

  if (cached) {
    return new Response(cached.body, cached.init)
  }

  const response = await next()

  // Cache successful responses
  if (response.status === 200) {
    const body = await response.clone().text()
    await cache.set(cacheKey, { body, init: { headers: response.headers } })
  }

  return response
}

Anti-Patterns ​

Forgetting to call next() ​

If middleware doesn't call next(), the request never reaches the route handler. This is valid for short-circuiting (auth redirects), but a bug if unintentional:

tsx
// Bug: forgot to return next()
export const middleware: MiddlewareHandler = async (request, context, next) => {
  context.set('timestamp', Date.now())
  // Missing: return next()
}

// Fix: always call and return next() unless intentionally short-circuiting
export const middleware: MiddlewareHandler = async (request, context, next) => {
  context.set('timestamp', Date.now())
  return next()
}

Async errors in middleware ​

Unhandled async errors in middleware crash the request. Always handle errors or let them propagate to the error boundary:

tsx
// Bad: unhandled promise rejection
export const middleware: MiddlewareHandler = async (request, context, next) => {
  const user = await getUser(request)  // Might throw!
  context.set('user', user)
  return next()
}

// Good: handle the error
export const middleware: MiddlewareHandler = async (request, context, next) => {
  try {
    const user = await getUser(request)
    context.set('user', user)
  } catch (err) {
    return Response.redirect('/login')
  }
  return next()
}

Middleware ordering assumptions ​

Middleware at the same level runs in alphabetical file order. Don't rely on implicit ordering — use named middleware in route config for explicit control:

tsx
export const config = {
  middleware: ['auth', 'rateLimit', 'logging']  // Explicit order
}

Heavy computation in middleware ​

Middleware runs on every request to its route. Avoid expensive operations (database queries, API calls) unless necessary. Use caching for expensive middleware results.

Released under the MIT License.