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 in TanStack Start. They enable type-safe, secure communication between client and server with automatic serialization and built-in middleware support.
What are Server Functions?
Server Functions are functions that run exclusively on the server but can be called from anywhere in your application. They provide:
- Type Safety: Full TypeScript inference from input to output
- Automatic Serialization: Built-in handling of complex data types
- Secure by Default: Server code never ships to the client
- Middleware Support: Composable middleware for auth, validation, and more
- HTTP Method Control: Choose GET or POST for each function
Creating Server Functions
Basic Server Function
Create a server function using createServerFn():import { createServerFn } from '@tanstack/react-start'
// Simple GET request (default)
const getPosts = createServerFn().handler(async () => {
const res = await fetch('https://api.example.com/posts')
return res.json()
})
// Use in component
function PostsList() {
const [posts, setPosts] = useState([])
useEffect(() => {
getPosts().then(setPosts)
}, [])
return <div>{/* render posts */}</div>
}
POST Server Function with Input
Use POST for mutations and when passing data:import { createServerFn } from '@tanstack/react-start'
const createPost = createServerFn({ method: 'POST' })
.inputValidator((data: { title: string; body: string }) => data)
.handler(async ({ data }) => {
const res = await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
})
return res.json()
})
// Call from component
function CreatePostForm() {
const handleSubmit = async (e) => {
e.preventDefault()
const formData = new FormData(e.target)
await createPost({
data: {
title: formData.get('title'),
body: formData.get('body')
}
})
}
return <form onSubmit={handleSubmit}>{/* form fields */}</form>
}
Use schema validation for type-safe inputs:import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1),
body: z.string().min(10)
})
const createPost = createServerFn({ method: 'POST' })
.inputValidator(postSchema)
.handler(async ({ data }) => {
// data is fully typed as { title: string; body: string }
const res = await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
})
return res.json()
})
Access Server Context
Access request information and server-only resources:import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
const getUser = createServerFn().handler(async ({ context }) => {
// Access request headers
const headers = getRequestHeaders()
const authToken = headers.get('authorization')
// Access context from middleware
const userId = context.userId
// Server-only code (database, env vars, etc.)
const user = await db.users.findUnique({ where: { id: userId } })
return user
})
Server Function Middleware
Compose reusable logic with middleware:
import { createServerFn, createMiddleware } from '@tanstack/react-start'
// Auth middleware
const authMiddleware = createMiddleware().server(async ({ next, context }) => {
const session = await getSession()
if (!session) {
throw new Response('Unauthorized', { status: 401 })
}
return next({
context: {
userId: session.userId,
role: session.role
}
})
})
// Apply middleware to server function
const deletePost = createServerFn({ method: 'POST' })
.middleware([authMiddleware])
.inputValidator((id: string) => id)
.handler(async ({ data, context }) => {
// context.userId is available from middleware
await db.posts.delete({
where: { id: data, userId: context.userId }
})
return { success: true }
})
Using useServerFn Hook
For better integration with router navigation:
import { useServerFn } from '@tanstack/react-start'
import { createServerFn, redirect } from '@tanstack/react-start'
const loginFn = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
const session = await authenticate(data)
if (!session) {
throw new Error('Invalid credentials')
}
// Redirect after successful login
throw redirect({ to: '/dashboard' })
})
function LoginForm() {
// useServerFn handles redirects automatically
const login = useServerFn(loginFn)
const handleSubmit = async (e) => {
e.preventDefault()
await login({ data: { email, password } })
// Automatically navigates on redirect
}
return <form onSubmit={handleSubmit}>{/* form */}</form>
}
Streaming Responses
Return ReadableStream for progressive data loading:
import { createServerFn } from '@tanstack/react-start'
const streamData = createServerFn().handler(async () => {
return new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 500))
controller.enqueue({ count: i })
}
controller.close()
}
})
})
function StreamingComponent() {
const [data, setData] = useState([])
useEffect(() => {
streamData().then(async (stream) => {
const reader = stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
setData(prev => [...prev, value])
}
})
}, [])
return <div>{data.map(d => <div key={d.count}>{d.count}</div>)}</div>
}
Advanced Patterns
Handle file uploads and form submissions:
const uploadFile = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
// data is FormData
const file = data.get('file') as File
const buffer = await file.arrayBuffer()
// Process file server-side
const url = await uploadToStorage(buffer, file.name)
return { url }
})
// Call with FormData
function UploadForm() {
const handleSubmit = async (e) => {
e.preventDefault()
const formData = new FormData(e.target)
const result = await uploadFile({ data: formData })
console.log('Uploaded:', result.url)
}
return (
<form onSubmit={handleSubmit}>
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
)
}
Error Handling
const fetchPost = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
const res = await fetch(`https://api.example.com/posts/${data}`)
if (!res.ok) {
if (res.status === 404) {
throw notFound()
}
throw new Error('Failed to fetch post')
}
return res.json()
})
// Handle errors in component
function Post({ id }) {
const [error, setError] = useState(null)
const loadPost = async () => {
try {
const post = await fetchPost({ data: id })
setPost(post)
} catch (err) {
setError(err.message)
}
}
return error ? <div>Error: {error}</div> : <div>{/* post */}</div>
}
Best Practices
- Use GET for reads: GET requests are cached by default and support URL parameters
- Use POST for mutations: POST requests are never cached and support larger payloads
- Validate inputs: Always use input validators for type safety and security
- Keep functions focused: Each server function should do one thing well
- Use middleware for cross-cutting concerns: Auth, logging, rate limiting, etc.
- Handle errors gracefully: Return meaningful error messages to the client
- Type everything: Leverage TypeScript for end-to-end type safety
Learn More