Fix api-routes to use current createFileRoute API

The previous pattern used createAPIFileRoute which is deprecated.
Updated to use createFileRoute with server.handlers property which
is the current API for server routes in TanStack Start.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Deckard Gerritsen
2026-01-16 16:00:11 +08:00
parent b281de6482
commit 33eea1cab2
+147 -117
View File
@@ -1,10 +1,10 @@
# api-routes: Create API Routes for External Consumers
# api-routes: Create Server Routes for External Consumers
## Priority: MEDIUM
## Explanation
While server functions are ideal for internal RPC, API routes provide traditional REST endpoints for external consumers, webhooks, and integrations. Use API routes when you need standard HTTP semantics, custom response formats, or third-party compatibility.
While server functions are ideal for internal RPC, server routes provide traditional REST endpoints for external consumers, webhooks, and integrations. Use server routes when you need standard HTTP semantics, custom response formats, or third-party compatibility.
## Bad Example
@@ -25,37 +25,41 @@ export const getUsers = createServerFn()
// No versioning, no standard REST semantics
```
## Good Example: Basic API Route
## Good Example: Basic Server Route
```tsx
// app/routes/api/users.ts
// routes/api/users.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
export const APIRoute = createAPIFileRoute('/api/users')({
GET: async ({ request }) => {
const users = await db.users.findMany({
select: { id: true, name: true, email: true },
})
export const Route = createFileRoute('/api/users')({
server: {
handlers: {
GET: async ({ request }) => {
const users = await db.users.findMany({
select: { id: true, name: true, email: true },
})
return json(users, {
headers: {
'Cache-Control': 'public, max-age=60',
return json(users, {
headers: {
'Cache-Control': 'public, max-age=60',
},
})
},
})
},
POST: async ({ request }) => {
const body = await request.json()
POST: async ({ request }) => {
const body = await request.json()
// Validate input
const parsed = createUserSchema.safeParse(body)
if (!parsed.success) {
return json({ error: parsed.error.flatten() }, { status: 400 })
}
// Validate input
const parsed = createUserSchema.safeParse(body)
if (!parsed.success) {
return json({ error: parsed.error.flatten() }, { status: 400 })
}
const user = await db.users.create({ data: parsed.data })
return json(user, { status: 201 })
const user = await db.users.create({ data: parsed.data })
return json(user, { status: 201 })
},
},
},
})
```
@@ -63,134 +67,159 @@ export const APIRoute = createAPIFileRoute('/api/users')({
## Good Example: Webhook Handler
```tsx
// app/routes/api/webhooks/stripe.ts
import { createAPIFileRoute } from '@tanstack/react-start/api'
// routes/api/webhooks/stripe.ts
import { createFileRoute } from '@tanstack/react-router'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export const APIRoute = createAPIFileRoute('/api/webhooks/stripe')({
POST: async ({ request }) => {
const signature = request.headers.get('stripe-signature')
if (!signature) {
return new Response('Missing signature', { status: 400 })
}
export const Route = createFileRoute('/api/webhooks/stripe')({
server: {
handlers: {
POST: async ({ request }) => {
const signature = request.headers.get('stripe-signature')
if (!signature) {
return new Response('Missing signature', { status: 400 })
}
// Get raw body for signature verification
const rawBody = await request.text()
// Get raw body for signature verification
const rawBody = await request.text()
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return new Response('Invalid signature', { status: 400 })
}
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return new Response('Invalid signature', { status: 400 })
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object)
break
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
// Handle the event
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object)
break
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return new Response('OK', { status: 200 })
return new Response('OK', { status: 200 })
},
},
},
})
```
## Good Example: RESTful Resource
## Good Example: RESTful Resource with Dynamic Params
```tsx
// app/routes/api/posts/$postId.ts
// routes/api/posts/$postId.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
export const APIRoute = createAPIFileRoute('/api/posts/$postId')({
GET: async ({ params }) => {
const post = await db.posts.findUnique({
where: { id: params.postId },
})
export const Route = createFileRoute('/api/posts/$postId')({
server: {
handlers: {
GET: async ({ params }) => {
const post = await db.posts.findUnique({
where: { id: params.postId },
})
if (!post) {
return json({ error: 'Post not found' }, { status: 404 })
}
if (!post) {
return json({ error: 'Post not found' }, { status: 404 })
}
return json(post)
},
return json(post)
},
PUT: async ({ request, params }) => {
const body = await request.json()
const parsed = updatePostSchema.safeParse(body)
PUT: async ({ request, params }) => {
const body = await request.json()
const parsed = updatePostSchema.safeParse(body)
if (!parsed.success) {
return json({ error: parsed.error.flatten() }, { status: 400 })
}
if (!parsed.success) {
return json({ error: parsed.error.flatten() }, { status: 400 })
}
const post = await db.posts.update({
where: { id: params.postId },
data: parsed.data,
})
const post = await db.posts.update({
where: { id: params.postId },
data: parsed.data,
})
return json(post)
},
return json(post)
},
DELETE: async ({ params }) => {
await db.posts.delete({ where: { id: params.postId } })
return new Response(null, { status: 204 })
DELETE: async ({ params }) => {
await db.posts.delete({ where: { id: params.postId } })
return new Response(null, { status: 204 })
},
},
},
})
```
## Good Example: With Authentication
## Good Example: With Route-Level Middleware
```tsx
// app/routes/api/protected/data.ts
// routes/api/protected/data.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
import { verifyApiKey } from '@/lib/auth'
import { apiKeyMiddleware } from '@/lib/middleware'
export const APIRoute = createAPIFileRoute('/api/protected/data')({
GET: async ({ request }) => {
// API key authentication
const apiKey = request.headers.get('x-api-key')
if (!apiKey) {
return json({ error: 'Missing API key' }, { status: 401 })
}
const client = await verifyApiKey(apiKey)
if (!client) {
return json({ error: 'Invalid API key' }, { status: 401 })
}
// Rate limit check
const withinLimits = await checkRateLimit(client.id)
if (!withinLimits) {
return json({ error: 'Rate limit exceeded' }, {
status: 429,
headers: { 'Retry-After': '60' },
})
}
const data = await fetchDataForClient(client.id)
return json(data)
export const Route = createFileRoute('/api/protected/data')({
server: {
// Middleware applies to all handlers in this route
middleware: [apiKeyMiddleware],
handlers: {
GET: async ({ request, context }) => {
// context.client available from middleware
const data = await fetchDataForClient(context.client.id)
return json(data)
},
},
},
})
```
## Server Functions vs API Routes
## Good Example: Using createHandlers for Handler-Specific Middleware
| Feature | Server Functions | API Routes |
|---------|-----------------|------------|
```tsx
// routes/api/admin/users.ts
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/api/admin/users')({
server: {
middleware: [authMiddleware], // All handlers require auth
handlers: (createHandlers) => ({
GET: createHandlers.GET(async ({ context }) => {
const users = await db.users.findMany()
return json(users)
}),
// DELETE requires additional admin middleware
DELETE: createHandlers.DELETE({
middleware: [adminOnlyMiddleware],
handler: async ({ request, context }) => {
const { userId } = await request.json()
await db.users.delete({ where: { id: userId } })
return json({ deleted: true })
},
}),
}),
},
})
```
## Server Functions vs Server Routes
| Feature | Server Functions | Server Routes |
|---------|-----------------|--------------|
| Primary use | Internal RPC | External consumers |
| Type safety | Full end-to-end | Manual |
| Response format | JSON (automatic) | Any (manual) |
@@ -200,9 +229,10 @@ export const APIRoute = createAPIFileRoute('/api/protected/data')({
## Context
- API routes live in `app/routes/api/` directory
- Server routes use `createFileRoute` with a `server.handlers` property
- Support all HTTP methods: GET, POST, PUT, PATCH, DELETE, etc.
- Use `json()` helper for JSON responses
- Return `Response` objects for custom formats
- Handler receives `{ request, params }` object
- Ideal for: webhooks, public APIs, file downloads, third-party integrations
- Consider versioning: `/api/v1/users` for public APIs