Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tanstack/router/llms.txt

Use this file to discover all available pages before exploring further.

Middleware

Middleware in TanStack Start enables you to compose reusable logic for authentication, logging, validation, and more. Middleware runs on both request-level (for routes) and function-level (for server functions).

What is Middleware?

Middleware is composable logic that runs before your route handlers or server functions. It can:
  • Authenticate users: Verify sessions and tokens
  • Transform data: Validate and sanitize inputs
  • Modify context: Add data available to downstream handlers
  • Control flow: Short-circuit execution with responses
  • Log and monitor: Track requests and performance

Types of Middleware

TanStack Start has two types of middleware:
  1. Request Middleware: Runs for every request to a route
  2. Function Middleware: Runs for server function calls

Creating Middleware

1

Basic Request Middleware

Create middleware that runs for route requests:
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware().server(async ({ next, request, pathname }) => {
  console.log(`Incoming request: ${request.method} ${pathname}`)
  
  const result = await next()
  
  console.log(`Request completed: ${pathname}`)
  
  return result
})
2

Apply to Routes

Apply middleware to specific routes:
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await getSession(request)
  
  if (!session) {
    throw new Response('Unauthorized', { status: 401 })
  }
  
  return next({
    context: {
      userId: session.userId,
      role: session.role
    }
  })
})

export const Route = createFileRoute('/dashboard')(
  {
    server: {
      middleware: [authMiddleware]
    },
    component: DashboardComponent
  }
)

function DashboardComponent() {
  return <div>Dashboard</div>
}
3

Function Middleware

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

const rateLimitMiddleware = createMiddleware()
  .server(async ({ next, context }) => {
    const userId = context.userId
    const allowed = await checkRateLimit(userId)
    
    if (!allowed) {
      throw new Response('Rate limit exceeded', { status: 429 })
    }
    
    return next()
  })

const apiCall = createServerFn({ method: 'POST' })
  .middleware([rateLimitMiddleware])
  .handler(async ({ data }) => {
    // This runs only if rate limit check passes
    return await processApiCall(data)
  })

Middleware Context

Pass data between middleware and handlers:
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'

const authMiddleware = createMiddleware().server(async ({ next }) => {
  const headers = getRequestHeaders()
  const token = headers.get('authorization')?.replace('Bearer ', '')
  
  if (!token) {
    throw new Response('Unauthorized', { status: 401 })
  }
  
  const user = await verifyToken(token)
  
  // Add user to context
  return next({
    context: {
      user: {
        id: user.id,
        email: user.email,
        role: user.role
      }
    }
  })
})

const getProfile = createServerFn()
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    // Access user from context
    const user = context.user
    
    return {
      id: user.id,
      email: user.email,
      profile: await db.profiles.findUnique({ where: { userId: user.id } })
    }
  })

Composing Middleware

Chain multiple middleware together:
import { createMiddleware } from '@tanstack/react-start'

// Parent middleware
const parentMiddleware = createMiddleware().server(async ({ next }) => {
  console.log('Parent: before')
  const result = await next()
  console.log('Parent: after')
  return result
})

// Child middleware that includes parent
const childMiddleware = createMiddleware()
  .middleware([parentMiddleware])
  .server(async ({ next }) => {
    console.log('Child: before')
    const result = await next()
    console.log('Child: after')
    return result
  })

// Apply composed middleware
const handler = createServerFn()
  .middleware([childMiddleware])
  .handler(async () => {
    console.log('Handler')
    return { success: true }
  })

// Execution order:
// 1. Parent: before
// 2. Child: before
// 3. Handler
// 4. Child: after
// 5. Parent: after

Route Middleware Patterns

Middleware for API Routes

Apply middleware to server route handlers:
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'

const corsMiddleware = createMiddleware().server(async ({ next }) => {
  const result = await next()
  
  result.response.headers.set('Access-Control-Allow-Origin', '*')
  result.response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
  
  return result
})

const jsonMiddleware = createMiddleware().server(async ({ next, request }) => {
  const headers = getRequestHeaders()
  const contentType = headers.get('content-type')
  
  if (contentType !== 'application/json') {
    throw new Response('Content-Type must be application/json', { status: 400 })
  }
  
  return next()
})

export const Route = createFileRoute('/api/users')(
  {
    server: {
      middleware: [corsMiddleware, jsonMiddleware],
      handlers: {
        GET: async ({ request }) => {
          const users = await db.users.findMany()
          return Response.json(users)
        },
        POST: async ({ request }) => {
          const data = await request.json()
          const user = await db.users.create({ data })
          return Response.json(user, { status: 201 })
        }
      }
    }
  }
)

Global Request Middleware

Apply middleware to all requests:
// src/app.tsx
import { createStart } from '@tanstack/react-start'
import { createMiddleware } from '@tanstack/react-start'

const globalLoggingMiddleware = createMiddleware().server(async ({ next, pathname }) => {
  const start = Date.now()
  
  const result = await next()
  
  const duration = Date.now() - start
  console.log(`[${pathname}] ${duration}ms`)
  
  return result
})

export const startInstance = createStart({
  requestMiddleware: [globalLoggingMiddleware]
})

Input Validation Middleware

Validate and transform input data:
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const validationMiddleware = createMiddleware()
  .inputValidator(z.object({
    email: z.string().email(),
    age: z.number().min(18)
  }))
  .server(async ({ data, next }) => {
    // data is validated and typed
    console.log('Valid data:', data)
    
    return next()
  })

const registerUser = createServerFn({ method: 'POST' })
  .middleware([validationMiddleware])
  .handler(async ({ data }) => {
    // data is already validated by middleware
    return await db.users.create({ data })
  })

Client-Side Middleware

Run middleware on the client before sending requests:
import { createMiddleware, createServerFn } from '@tanstack/react-start'

const clientAuthMiddleware = createMiddleware()
  .client(async ({ next, sendContext }) => {
    // Get auth token from client storage
    const token = localStorage.getItem('authToken')
    
    // Send token in context to server
    return next({
      sendContext: {
        token
      },
      headers: {
        'Authorization': `Bearer ${token}`
      }
    })
  })
  .server(async ({ context, next }) => {
    // Access token from client context
    const { token } = context
    const user = await verifyToken(token)
    
    return next({
      context: { user }
    })
  })

const protectedAction = createServerFn({ method: 'POST' })
  .middleware([clientAuthMiddleware])
  .handler(async ({ context }) => {
    // context.user is available from middleware
    return { userId: context.user.id }
  })

Error Handling in Middleware

Handle errors gracefully:
import { createMiddleware } from '@tanstack/react-start'

const errorHandlingMiddleware = createMiddleware().server(async ({ next }) => {
  try {
    return await next()
  } catch (error) {
    // Log error
    console.error('Middleware error:', error)
    
    // Return error response
    if (error instanceof Error) {
      throw new Response(
        JSON.stringify({ error: error.message }),
        {
          status: 500,
          headers: { 'Content-Type': 'application/json' }
        }
      )
    }
    
    throw error
  }
})

Conditional Middleware

Run middleware based on conditions:
import { createMiddleware } from '@tanstack/react-start'

const conditionalMiddleware = createMiddleware().server(async ({ next, pathname }) => {
  // Only apply to API routes
  if (pathname.startsWith('/api')) {
    console.log('API request:', pathname)
    
    const result = await next()
    
    // Add API-specific headers
    result.response.headers.set('X-API-Version', '1.0')
    
    return result
  }
  
  // Skip middleware for other routes
  return next()
})

Response Modification

Modify responses in middleware:
import { createMiddleware } from '@tanstack/react-start'

const headerMiddleware = createMiddleware().server(async ({ next }) => {
  const result = await next()
  
  // Add security headers
  result.response.headers.set('X-Content-Type-Options', 'nosniff')
  result.response.headers.set('X-Frame-Options', 'DENY')
  result.response.headers.set('X-XSS-Protection', '1; mode=block')
  
  return result
})

const cacheMiddleware = createMiddleware().server(async ({ next, pathname }) => {
  const result = await next()
  
  // Set cache headers for static content
  if (pathname.startsWith('/static')) {
    result.response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
  }
  
  return result
})

Testing Middleware

Test middleware in isolation:
import { describe, it, expect } from 'vitest'
import { createMiddleware } from '@tanstack/react-start'

describe('authMiddleware', () => {
  it('should allow authenticated requests', async () => {
    const middleware = createMiddleware().server(async ({ next, context }) => {
      if (!context.userId) {
        throw new Response('Unauthorized', { status: 401 })
      }
      return next()
    })
    
    const mockNext = async () => ({ response: new Response('OK') })
    const mockContext = { userId: '123' }
    
    const result = await middleware.options.server({
      next: mockNext,
      context: mockContext,
      request: new Request('http://localhost/test'),
      pathname: '/test'
    })
    
    expect(result.response.status).toBe(200)
  })
  
  it('should reject unauthenticated requests', async () => {
    const middleware = createMiddleware().server(async ({ next, context }) => {
      if (!context.userId) {
        throw new Response('Unauthorized', { status: 401 })
      }
      return next()
    })
    
    const mockNext = async () => ({ response: new Response('OK') })
    const mockContext = {}
    
    await expect(
      middleware.options.server({
        next: mockNext,
        context: mockContext,
        request: new Request('http://localhost/test'),
        pathname: '/test'
      })
    ).rejects.toThrow()
  })
})

Best Practices

  • Keep middleware focused: Each middleware should do one thing well
  • Order matters: Place authentication before authorization
  • Use types: Leverage TypeScript for context typing
  • Handle errors: Always implement error handling
  • Avoid side effects: Keep middleware pure when possible
  • Test thoroughly: Unit test middleware in isolation
  • Document context: Clearly document what context each middleware adds
  • Compose wisely: Reuse middleware through composition

Learn More