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.

API Routes

API routes in TanStack Start allow you to create REST API endpoints alongside your application routes. They provide a type-safe way to build backend APIs with full access to server-side resources.

Creating API Routes

API routes are created using the server option in route files:
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/users')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        const users = await db.users.findAll()
        return Response.json(users)
      },
    },
  },
})

HTTP Methods

Support multiple HTTP methods in a single route:
export const Route = createFileRoute('/api/posts')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        const posts = await db.posts.findAll()
        return Response.json(posts)
      },
      
      POST: async ({ request }) => {
        const body = await request.json()
        const post = await db.posts.create(body)
        return Response.json(post, { status: 201 })
      },
      
      DELETE: async ({ request }) => {
        const url = new URL(request.url)
        const id = url.searchParams.get('id')
        await db.posts.delete(id)
        return new Response(null, { status: 204 })
      },
    },
  },
})

Dynamic Routes

Use route parameters in API routes:
export const Route = createFileRoute('/api/users/$userId')({
  server: {
    handlers: {
      GET: async ({ params, request }) => {
        const user = await db.users.findById(params.userId)
        
        if (!user) {
          return Response.json(
            { error: 'User not found' },
            { status: 404 }
          )
        }
        
        return Response.json(user)
      },
      
      PUT: async ({ params, request }) => {
        const body = await request.json()
        const user = await db.users.update(params.userId, body)
        return Response.json(user)
      },
      
      DELETE: async ({ params }) => {
        await db.users.delete(params.userId)
        return new Response(null, { status: 204 })
      },
    },
  },
})

Request Handling

Parsing Request Body

export const Route = createFileRoute('/api/posts')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        // JSON body
        const data = await request.json()
        
        // FormData
        const formData = await request.formData()
        const title = formData.get('title')
        
        // Text
        const text = await request.text()
        
        // Binary data
        const buffer = await request.arrayBuffer()
        
        return Response.json({ success: true })
      },
    },
  },
})

Query Parameters

export const Route = createFileRoute('/api/search')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        const url = new URL(request.url)
        const query = url.searchParams.get('q')
        const limit = parseInt(url.searchParams.get('limit') || '10')
        const offset = parseInt(url.searchParams.get('offset') || '0')
        
        const results = await db.posts.search({
          query,
          limit,
          offset,
        })
        
        return Response.json(results)
      },
    },
  },
})

Request Headers

export const Route = createFileRoute('/api/protected')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        const auth = request.headers.get('Authorization')
        const contentType = request.headers.get('Content-Type')
        const userAgent = request.headers.get('User-Agent')
        
        if (!auth) {
          return new Response('Unauthorized', { status: 401 })
        }
        
        return Response.json({ authenticated: true })
      },
    },
  },
})

Response Handling

JSON Responses

export const Route = createFileRoute('/api/data')({
  server: {
    handlers: {
      GET: async () => {
        return Response.json(
          { message: 'Success', data: [...] },
          { 
            status: 200,
            headers: {
              'Cache-Control': 'public, max-age=3600',
            },
          }
        )
      },
    },
  },
})

Custom Headers

export const Route = createFileRoute('/api/file')({
  server: {
    handlers: {
      GET: async () => {
        const file = await readFile('path/to/file.pdf')
        
        return new Response(file, {
          headers: {
            'Content-Type': 'application/pdf',
            'Content-Disposition': 'attachment; filename="file.pdf"',
            'Cache-Control': 'no-cache',
          },
        })
      },
    },
  },
})

Streaming Responses

export const Route = createFileRoute('/api/stream')({
  server: {
    handlers: {
      GET: async () => {
        const stream = new ReadableStream({
          async start(controller) {
            for (let i = 0; i < 10; i++) {
              const data = await fetchChunk(i)
              controller.enqueue(
                new TextEncoder().encode(JSON.stringify(data) + '\n')
              )
              await new Promise((r) => setTimeout(r, 1000))
            }
            controller.close()
          },
        })
        
        return new Response(stream, {
          headers: {
            'Content-Type': 'application/x-ndjson',
            'Cache-Control': 'no-cache',
          },
        })
      },
    },
  },
})

Middleware with API Routes

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

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

const rateLimiter = createMiddleware().server(async ({ request, next }) => {
  const ip = request.headers.get('x-forwarded-for')
  
  if (await isRateLimited(ip)) {
    throw new Response('Too many requests', { status: 429 })
  }
  
  return next()
})

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

Error Handling

Custom Error Responses

export const Route = createFileRoute('/api/users/$userId')({
  server: {
    handlers: {
      GET: async ({ params }) => {
        try {
          const user = await db.users.findById(params.userId)
          return Response.json(user)
        } catch (error) {
          if (error instanceof NotFoundError) {
            return Response.json(
              { error: 'User not found' },
              { status: 404 }
            )
          }
          
          if (error instanceof ValidationError) {
            return Response.json(
              { error: 'Invalid user ID', details: error.details },
              { status: 400 }
            )
          }
          
          console.error('Unexpected error:', error)
          return Response.json(
            { error: 'Internal server error' },
            { status: 500 }
          )
        }
      },
    },
  },
})

Error Middleware

const errorHandler = createMiddleware().server(async ({ next }) => {
  try {
    return await next()
  } catch (error) {
    console.error('API Error:', error)
    
    return Response.json(
      { 
        error: 'Internal server error',
        message: process.env.NODE_ENV === 'development' 
          ? error.message 
          : 'An error occurred',
      },
      { status: 500 }
    )
  }
})

CORS Support

const corsMiddleware = createMiddleware().server(
  async ({ request, next }) => {
    const origin = request.headers.get('origin')
    
    // Handle preflight
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': origin || '*',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Max-Age': '86400',
        },
      })
    }
    
    const result = await next()
    
    // Add CORS headers to response
    result.response.headers.set(
      'Access-Control-Allow-Origin',
      origin || '*'
    )
    result.response.headers.set(
      'Access-Control-Allow-Credentials',
      'true'
    )
    
    return result
  }
)

export const Route = createFileRoute('/api/public')({
  server: {
    middleware: [corsMiddleware],
    handlers: {
      GET: async () => Response.json({ message: 'Hello' }),
    },
  },
})

Webhooks

Handle webhook endpoints:
import crypto from 'crypto'

function verifySignature(payload: string, signature: string, secret: string) {
  const hmac = crypto.createHmac('sha256', secret)
  const digest = hmac.update(payload).digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  )
}

export const Route = createFileRoute('/api/webhooks/stripe')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const payload = await request.text()
        const signature = request.headers.get('stripe-signature')
        
        if (!verifySignature(payload, signature, process.env.STRIPE_SECRET)) {
          return new Response('Invalid signature', { status: 401 })
        }
        
        const event = JSON.parse(payload)
        
        switch (event.type) {
          case 'payment_intent.succeeded':
            await handlePaymentSuccess(event.data)
            break
          case 'payment_intent.failed':
            await handlePaymentFailure(event.data)
            break
        }
        
        return Response.json({ received: true })
      },
    },
  },
})

File Uploads

export const Route = createFileRoute('/api/upload')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const formData = await request.formData()
        const file = formData.get('file') as File
        
        if (!file) {
          return Response.json(
            { error: 'No file provided' },
            { status: 400 }
          )
        }
        
        // Validate file type
        if (!file.type.startsWith('image/')) {
          return Response.json(
            { error: 'Only images allowed' },
            { status: 400 }
          )
        }
        
        // Validate file size (5MB max)
        if (file.size > 5 * 1024 * 1024) {
          return Response.json(
            { error: 'File too large' },
            { status: 400 }
          )
        }
        
        // Save file
        const buffer = await file.arrayBuffer()
        const filename = `${Date.now()}-${file.name}`
        await fs.writeFile(`/uploads/${filename}`, Buffer.from(buffer))
        
        return Response.json({
          filename,
          url: `/uploads/${filename}`,
        })
      },
    },
  },
})

Pagination

export const Route = createFileRoute('/api/posts')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        const url = new URL(request.url)
        const page = parseInt(url.searchParams.get('page') || '1')
        const limit = parseInt(url.searchParams.get('limit') || '20')
        const offset = (page - 1) * limit
        
        const [posts, total] = await Promise.all([
          db.posts.findMany({ limit, offset }),
          db.posts.count(),
        ])
        
        return Response.json({
          data: posts,
          pagination: {
            page,
            limit,
            total,
            pages: Math.ceil(total / limit),
          },
        })
      },
    },
  },
})

REST API Best Practices

Consistent Response Format

type ApiResponse<T> = {
  success: boolean
  data?: T
  error?: string
  meta?: {
    timestamp: string
    version: string
  }
}

function apiResponse<T>(data: T, status = 200): Response {
  return Response.json(
    {
      success: status < 400,
      data,
      meta: {
        timestamp: new Date().toISOString(),
        version: '1.0',
      },
    } as ApiResponse<T>,
    { status }
  )
}

export const Route = createFileRoute('/api/users')({
  server: {
    handlers: {
      GET: async () => {
        const users = await db.users.findAll()
        return apiResponse(users)
      },
    },
  },
})

API Versioning

export const Route = createFileRoute('/api/v1/users')({
  server: {
    handlers: {
      GET: async () => {
        // Version 1 implementation
        return Response.json(await db.users.findAll())
      },
    },
  },
})

export const RouteV2 = createFileRoute('/api/v2/users')({
  server: {
    handlers: {
      GET: async () => {
        // Version 2 with different response format
        const users = await db.users.findAllWithProfiles()
        return Response.json(users)
      },
    },
  },
})

Best Practices

  1. Use Appropriate HTTP Methods
    • GET for reading data
    • POST for creating resources
    • PUT/PATCH for updating resources
    • DELETE for removing resources
  2. Return Proper Status Codes
    • 200: Success
    • 201: Created
    • 204: No Content
    • 400: Bad Request
    • 401: Unauthorized
    • 403: Forbidden
    • 404: Not Found
    • 500: Server Error
  3. Validate Input
    • Always validate request data
    • Return meaningful error messages
    • Use validation libraries
  4. Handle Errors Gracefully
    • Catch and handle all errors
    • Don’t expose internal errors to clients
    • Log errors for debugging
  5. Secure Your APIs
    • Implement authentication
    • Use rate limiting
    • Validate API keys
    • Enable CORS appropriately
  6. Document Your APIs
    • Use consistent naming
    • Document parameters and responses
    • Provide examples
  7. Performance
    • Implement pagination
    • Use caching headers
    • Optimize database queries

Next Steps

Build docs developers (and LLMs) love