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 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:
- Request Middleware - Runs for all requests (pages and API routes)
- 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
}
})
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],
})
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
-
Keep Middleware Focused
- Each middleware should do one thing well
- Compose multiple middleware for complex scenarios
-
Handle Errors Properly
- Always catch and handle errors in middleware
- Provide meaningful error responses
-
Be Careful with Context
- Only add necessary data to context
- Avoid large objects that need serialization
-
Order Matters
- Place authentication/validation middleware first
- Put logging/monitoring middleware early
- Error handlers should be at the start of the chain
-
Type Your Context
- Use TypeScript to type context additions
- Leverage type inference for better DX
-
Performance Considerations
- Avoid expensive operations in middleware
- Cache results when possible
- Use async operations wisely
-
Security First
- Validate all inputs
- Sanitize data before adding to context
- Use proper authentication and authorization
Next Steps