Skip to main content

Middleware

Middleware in TanStack Start allows you to intercept and modify requests, responses, and context at both the request and function levels. Use middleware for authentication, logging, error handling, and more.

Types of Middleware

TanStack Start supports two types of middleware:
  1. Request Middleware - Runs for all requests (pages and API routes)
  2. Function Middleware - Runs for specific server functions

Request Middleware

Request middleware runs on every request before rendering or handling:
import { createMiddleware } from '@tanstack/react-start'

const authMiddleware = createMiddleware().server(async ({ request, next }) => {
  const session = await getSession(request)
  
  if (!session) {
    throw redirect({ to: '/login' })
  }

  return next({
    context: {
      user: session.user,
    },
  })
})

export const Route = createFileRoute('/dashboard')({
  server: {
    middleware: [authMiddleware],
  },
  loader: async ({ context }) => {
    // context.user is available here
    return { user: context.user }
  },
})

Function Middleware

Function middleware runs specifically for server functions:
import { createMiddleware, createServerFn } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next }) => {
    const startTime = Date.now()
    const result = await next()
    console.log(`Request took ${Date.now() - startTime}ms`)
    return result
  })
  .server(async ({ next, serverFnMeta }) => {
    console.log(`Executing function: ${serverFnMeta.name}`)
    return await next()
  })

const fetchData = createServerFn({ method: 'GET' })
  .middleware([loggingMiddleware])
  .handler(async () => {
    return { data: 'Hello' }
  })

Creating Middleware

Basic Middleware

import { createMiddleware } from '@tanstack/react-start'

const simpleMiddleware = createMiddleware().server(async ({ request, next }) => {
  console.log('Processing request:', request.url)
  const result = await next()
  console.log('Request complete')
  return result
})

Client + Server Middleware

const timingMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next }) => {
    const clientTime = new Date()
    
    return next({
      sendContext: {
        clientTime,
      },
    })
  })
  .server(async ({ next, context }) => {
    const serverTime = new Date()
    const duration = serverTime.getTime() - context.clientTime.getTime()
    
    return next({
      sendContext: {
        serverTime,
        duration,
      },
    })
  })

Middleware Context

Adding Context

Middleware can add data to the context:
const userMiddleware = createMiddleware().server(async ({ request, next }) => {
  const userId = request.headers.get('x-user-id')
  const user = await db.users.findById(userId)
  
  return next({
    context: {
      user,
      isAdmin: user?.role === 'admin',
    },
  })
})

Accessing Context

Access context in route handlers and server functions:
export const Route = createFileRoute('/admin')({
  server: {
    middleware: [userMiddleware],
  },
  loader: async ({ context }) => {
    // context.user and context.isAdmin available
    if (!context.isAdmin) {
      throw redirect({ to: '/' })
    }
    
    return await getAdminData()
  },
})

Send Context

Send data back to the client:
const dataMiddleware = createMiddleware({ type: 'function' })
  .server(async ({ next }) => {
    const serverData = await getServerData()
    
    return next({
      sendContext: {
        timestamp: Date.now(),
        serverData,
      },
    })
  })
Access send context on the client:
const fetchData = createServerFn({ method: 'GET' })
  .middleware([dataMiddleware])
  .handler(async () => {
    return { result: 'data' }
  })

// In component:
const result = await fetchData()
// result.context.timestamp and result.context.serverData available

Common Middleware Patterns

Authentication

const requireAuth = createMiddleware().server(async ({ request, next }) => {
  const token = request.headers.get('Authorization')?.split(' ')[1]
  
  if (!token) {
    throw new Response('Unauthorized', { status: 401 })
  }
  
  try {
    const user = await verifyToken(token)
    return next({ context: { user } })
  } catch (error) {
    throw new Response('Invalid token', { status: 401 })
  }
})

Rate Limiting

const rateLimitCache = new Map<string, number[]>()

const rateLimit = createMiddleware().server(async ({ request, next }) => {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'
  const now = Date.now()
  const windowMs = 60 * 1000 // 1 minute
  const maxRequests = 100
  
  const requests = rateLimitCache.get(ip) || []
  const recentRequests = requests.filter((time) => now - time < windowMs)
  
  if (recentRequests.length >= maxRequests) {
    throw new Response('Too many requests', { status: 429 })
  }
  
  recentRequests.push(now)
  rateLimitCache.set(ip, recentRequests)
  
  return next()
})

Request Logging

const logger = createMiddleware().server(async ({ request, next }) => {
  const start = Date.now()
  const { pathname } = new URL(request.url)
  
  console.log(`→ ${request.method} ${pathname}`)
  
  try {
    const result = await next()
    const duration = Date.now() - start
    console.log(`← ${request.method} ${pathname} ${duration}ms`)
    return result
  } catch (error) {
    const duration = Date.now() - start
    console.error(`✗ ${request.method} ${pathname} ${duration}ms`, error)
    throw error
  }
})

CORS Headers

const cors = createMiddleware().server(async ({ request, next }) => {
  const origin = request.headers.get('origin')
  const result = await next()
  
  if (origin) {
    result.response.headers.set('Access-Control-Allow-Origin', origin)
    result.response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    result.response.headers.set('Access-Control-Allow-Headers', 'Content-Type')
  }
  
  return result
})

Error Handling

const errorHandler = createMiddleware().server(async ({ next }) => {
  try {
    return await next()
  } catch (error) {
    console.error('Middleware error:', error)
    
    if (error instanceof DatabaseError) {
      throw new Response('Database error', { status: 503 })
    }
    
    if (error instanceof ValidationError) {
      throw new Response(JSON.stringify(error.errors), { 
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      })
    }
    
    throw error
  }
})

Composing Middleware

Middleware Chains

Chain multiple middleware together:
const apiMiddleware = createMiddleware()
  .middleware([logger, cors, rateLimit, errorHandler])
  .server(async ({ next }) => {
    // All parent middleware runs before this
    return next()
  })

Reusable Middleware

Create reusable middleware for common patterns:
// middleware/auth.ts
export const requireRole = (role: string) => {
  return createMiddleware().server(async ({ context, next }) => {
    if (context.user?.role !== role) {
      throw new Response('Forbidden', { status: 403 })
    }
    return next()
  })
}

// In routes:
import { requireRole } from '~/middleware/auth'

export const Route = createFileRoute('/admin')({
  server: {
    middleware: [requireAuth, requireRole('admin')],
  },
})

API Route Middleware

Use middleware with API routes:
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'

const apiLogger = createMiddleware().server(async ({ request, next }) => {
  console.log(`API ${request.method} ${request.url}`)
  return next()
})

const validateApiKey = createMiddleware().server(async ({ request, next }) => {
  const apiKey = request.headers.get('x-api-key')
  
  if (!apiKey || !await isValidApiKey(apiKey)) {
    throw new Response('Invalid API key', { status: 401 })
  }
  
  return next()
})

export const Route = createFileRoute('/api/data')({
  server: {
    middleware: [apiLogger, validateApiKey],
    handlers: {
      GET: async ({ request }) => {
        const data = await fetchData()
        return Response.json(data)
      },
    },
  },
})

Global Middleware

Apply middleware to all routes using the Start configuration:
// app.config.ts
import { createStart } from '@tanstack/react-start'
import { logger, cors } from '~/middleware'

export default createStart({
  requestMiddleware: [logger, cors],
  functionMiddleware: [timingMiddleware],
})

Input Validation Middleware

Validate inputs in middleware:
import { z } from 'zod'

const validateInput = createMiddleware({ type: 'function' })
  .inputValidator(
    z.object({
      id: z.string().uuid(),
      name: z.string().min(1),
    })
  )
  .server(async ({ data, next }) => {
    // data is validated and typed
    console.log('Valid data:', data)
    return next()
  })

Middleware Execution Order

Middleware executes in order:
const first = createMiddleware().server(async ({ next }) => {
  console.log('1: before')
  const result = await next()
  console.log('1: after')
  return result
})

const second = createMiddleware().server(async ({ next }) => {
  console.log('2: before')
  const result = await next()
  console.log('2: after')
  return result
})

const fn = createServerFn({ method: 'GET' })
  .middleware([first, second])
  .handler(async () => {
    console.log('handler')
    return 'done'
  })

// Output:
// 1: before
// 2: before
// handler
// 2: after
// 1: after

Best Practices

  1. Keep Middleware Focused
    • Each middleware should do one thing well
    • Compose multiple middleware for complex scenarios
  2. Handle Errors Properly
    • Always catch and handle errors in middleware
    • Provide meaningful error responses
  3. Be Careful with Context
    • Only add necessary data to context
    • Avoid large objects that need serialization
  4. Order Matters
    • Place authentication/validation middleware first
    • Put logging/monitoring middleware early
    • Error handlers should be at the start of the chain
  5. Type Your Context
    • Use TypeScript to type context additions
    • Leverage type inference for better DX
  6. Performance Considerations
    • Avoid expensive operations in middleware
    • Cache results when possible
    • Use async operations wisely
  7. Security First
    • Validate all inputs
    • Sanitize data before adding to context
    • Use proper authentication and authorization

Next Steps

Build docs developers (and LLMs) love