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.
Server Functions
Server functions are the primary way to execute server-side code from your client components in TanStack Start. They provide type-safe, RPC-like functionality with automatic serialization and validation.
What are Server Functions?
Server functions are functions that:
- Run only on the server - Never bundled into client code
- Type-safe - Full TypeScript support from client to server
- Automatically serialized - Handle complex data types seamlessly
- HTTP-based - Use standard HTTP methods (GET, POST)
- Middleware-enabled - Support authentication, validation, and more
Creating Server Functions
Use createServerFn to define a server function:
import { createServerFn } from '@tanstack/react-start'
const getUser = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data: userId }) => {
// This code runs ONLY on the server
const user = await db.users.findById(userId)
return { id: user.id, name: user.name, email: user.email }
})
Reference: packages/start-client-core/src/createServerFn.ts:53-196
HTTP Methods
Server functions support GET and POST methods:
GET Requests
Ideal for data fetching without side effects:
const getPosts = createServerFn({ method: 'GET' })
.handler(async () => {
const posts = await db.posts.findAll()
return posts
})
// Usage
const posts = await getPosts()
GET requests:
- Serialize data in the URL query string
- Can be cached by browsers and CDNs
- Have size limitations (~1MB)
Reference: packages/start-server-core/src/server-functions-handler.ts:131-146
POST Requests
Use for mutations and large payloads:
const createPost = createServerFn({ method: 'POST' })
.inputValidator(z.object({
title: z.string(),
content: z.string(),
}))
.handler(async ({ data }) => {
const post = await db.posts.create(data)
return post
})
// Usage
const post = await createPost({
data: {
title: 'Hello World',
content: 'My first post'
}
})
POST requests:
- Send data in the request body
- Support larger payloads
- Can handle FormData for file uploads
Reference: packages/start-server-core/src/server-functions-handler.ts:38-65
Validate inputs before they reach your handler:
With Zod
import { z } from 'zod'
import { createServerFn } from '@tanstack/react-start'
const updateUser = createServerFn({ method: 'POST' })
.inputValidator(z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
}))
.handler(async ({ data }) => {
// data is fully validated and typed
await db.users.update(data.id, data)
return { success: true }
})
With Custom Validators
const myValidator = (input: unknown) => {
if (typeof input !== 'string') {
throw new Error('Expected string')
}
return input.toLowerCase()
}
const myServerFn = createServerFn({ method: 'POST' })
.inputValidator(myValidator)
.handler(async ({ data }) => {
// data is guaranteed to be a lowercase string
return data
})
Reference: packages/start-client-core/src/createServerFn.ts:749-773
Middleware
Add middleware to server functions for cross-cutting concerns:
import { createMiddleware, createServerFn } from '@tanstack/react-start'
// Create reusable middleware
const authMiddleware = createMiddleware()
.server(async ({ request, next }) => {
const session = await getSession(request.headers.get('cookie'))
if (!session) {
throw new Error('Unauthorized')
}
return next({ context: { userId: session.userId } })
})
// Apply middleware to server function
const getPrivateData = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
// context.userId is available from middleware
const data = await db.privateData.findByUserId(context.userId)
return data
})
Reference: packages/start-client-core/src/createServerFn.ts:68-90
Context Sharing
Share data between middleware and handlers:
const logMiddleware = createMiddleware()
.server(async ({ next }) => {
const startTime = Date.now()
const result = await next({
context: { startTime }
})
console.log(`Duration: ${Date.now() - startTime}ms`)
return result
})
const myFunction = createServerFn({ method: 'POST' })
.middleware([logMiddleware])
.handler(async ({ context }) => {
// context.startTime is available
console.log('Started at:', context.startTime)
return { success: true }
})
Reference: packages/start-client-core/src/createServerFn.ts:256-286
Error Handling
Handle errors gracefully:
const riskyOperation = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
try {
const result = await performOperation(data)
return { success: true, result }
} catch (error) {
console.error('Operation failed:', error)
throw new Error('Operation failed')
}
})
// Usage with error handling
try {
const result = await riskyOperation({ data: 'test' })
console.log('Success:', result)
} catch (error) {
console.error('Error:', error.message)
}
Reference: packages/start-server-core/src/server-functions-handler.ts:316-360
Handle file uploads and form submissions:
const uploadFile = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
// data is FormData when Content-Type is multipart/form-data
const file = data.get('file') as File
const title = data.get('title') as string
// Process the file
const buffer = await file.arrayBuffer()
const url = await storage.upload(buffer, file.name)
// Save to database
await db.files.create({ title, url })
return { url }
})
// Usage with FormData
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('title', 'My Document')
const result = await uploadFile({ data: formData })
Reference: packages/start-server-core/src/server-functions-handler.ts:85-128
Streaming Responses
Return streaming data from server functions:
import { createServerFn } from '@tanstack/react-start'
// Return a ReadableStream
const streamData = createServerFn({ method: 'GET' })
.handler(async () => {
return new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 100))
controller.enqueue({ index: i, data: `Item ${i}` })
}
controller.close()
},
})
})
// Or use an async generator
const streamWithGenerator = createServerFn({ method: 'GET' })
.handler(async function* () {
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 100))
yield { index: i, data: `Item ${i}` }
}
})
// Usage
const stream = await streamData()
const reader = stream.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log('Received:', value)
}
// Or with async generator
for await (const item of await streamWithGenerator()) {
console.log('Received:', item)
}
Reference: examples/react/start-streaming-data-from-server-functions/src/routes/index.tsx:58-89
Calling Server Functions
From Loaders
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await getPosts()
return { posts }
},
})
From Components
function CreatePost() {
const [title, setTitle] = useState('')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
await createPost({ data: { title, content: '' } })
// Redirect or show success message
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={e => setTitle(e.target.value)} />
<button type="submit">Create</button>
</form>
)
}
From Effects
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null)
useEffect(() => {
getUser({ data: userId }).then(setUser)
}, [userId])
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
Server-to-Server Calls
When calling a server function from another server function, use SSR RPC:
import { createSsrRpc } from '@tanstack/react-start'
// In one file
const getUserInternal = createServerFn({ method: 'GET' })
.handler(async ({ data: userId }) => {
return db.users.findById(userId)
})
// In another file
const getUserWithPosts = createServerFn({ method: 'GET' })
.handler(async ({ data: userId }) => {
// Direct call on the server - no HTTP roundtrip
const user = await getUserInternal({ data: userId })
const posts = await db.posts.findByUserId(userId)
return { user, posts }
})
Reference: packages/start-server-core/src/createSsrRpc.ts:8-26
Advanced Patterns
Composable Middleware
const authMiddleware = createMiddleware()
.server(async ({ next }) => {
const user = await authenticate()
return next({ context: { user } })
})
const rbacMiddleware = createMiddleware()
.server(async ({ next, context }) => {
if (!hasPermission(context.user, 'admin')) {
throw new Error('Forbidden')
}
return next()
})
// Compose multiple middleware
const adminAction = createServerFn({ method: 'POST' })
.middleware([authMiddleware, rbacMiddleware])
.handler(async () => {
// Only admins can reach here
})
Custom Fetch Options
// Pass custom fetch options
const result = await myServerFn({
data: { foo: 'bar' },
headers: { 'X-Custom': 'value' },
signal: abortController.signal,
})
Response Customization
import { getResponse } from '@tanstack/react-start/server'
const customResponse = createServerFn({ method: 'GET' })
.handler(async () => {
const response = getResponse()
// Set custom status and headers
response.status = 201
response.statusText = 'Created'
response.headers.set('X-Custom-Header', 'value')
return { data: 'Created' }
})
Reference: packages/start-server-core/src/request-response.ts
Security Best Practices
const safeFunction = createServerFn({ method: 'POST' })
.inputValidator(z.object({
userId: z.string().uuid(), // Only accept valid UUIDs
action: z.enum(['like', 'unlike']), // Whitelist actions
}))
.handler(async ({ data }) => {
// data is guaranteed to be valid
})
2. Check Authentication
const protectedFunction = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
// context.user is guaranteed to exist
})
3. Never Expose Secrets
// ❌ Bad - exposes secret to client
const badFunction = createServerFn({ method: 'GET' })
.handler(() => {
return { apiKey: process.env.SECRET_API_KEY }
})
// ✅ Good - keeps secret on server
const goodFunction = createServerFn({ method: 'GET' })
.handler(async () => {
const data = await fetchWithSecret(process.env.SECRET_API_KEY)
return data // Only return safe data
})
4. Rate Limiting
const rateLimitMiddleware = createMiddleware()
.server(async ({ request, next }) => {
const ip = request.headers.get('x-forwarded-for')
const limited = await checkRateLimit(ip)
if (limited) {
throw new Error('Rate limit exceeded')
}
return next()
})
1. Use GET for Cacheable Data
// GET requests can be cached
const getCacheableData = createServerFn({ method: 'GET' })
.handler(async () => {
// This response can be cached by CDN
return expensiveOperation()
})
2. Batch Requests
// Instead of multiple calls
const user = await getUser({ data: userId })
const posts = await getPosts({ data: userId })
// Batch into one
const getUserWithPosts = createServerFn({ method: 'GET' })
.handler(async ({ data: userId }) => {
const [user, posts] = await Promise.all([
db.users.findById(userId),
db.posts.findByUserId(userId),
])
return { user, posts }
})
3. Avoid Unnecessary Serialization
// Return only what you need
const getUser = createServerFn({ method: 'GET' })
.handler(async ({ data: userId }) => {
const user = await db.users.findById(userId)
// Only return necessary fields
return {
id: user.id,
name: user.name,
email: user.email,
// Don't include password, internal IDs, etc.
}
})
Next Steps