| name | start-core/server-functions |
| description | createServerFn (GET/POST), inputValidator (Zod or function), useServerFn hook, server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus), error handling (throw errors, redirect, notFound), streaming, FormData handling, file organization (.functions.ts, .server.ts). |
| type | sub-skill |
| library | tanstack-start |
| library_version | 1.166.2 |
| requires | ["start-core"] |
| sources | ["TanStack/router:docs/start/framework/react/guide/server-functions.md"] |
Server Functions
Server functions are type-safe RPCs created with createServerFn. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions.
CRITICAL: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside createServerFn, NOT in loaders directly.
CRITICAL: Do not use "use server" directives, getServerSideProps, or any Next.js/Remix server patterns. TanStack Start uses createServerFn exclusively.
Basic Usage
import { createServerFn } from '@tanstack/react-start'
const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
const saveData = createServerFn({ method: 'POST' }).handler(async () => {
return { success: true }
})
Calling from Loaders
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
component: PostList,
})
function PostList() {
const { posts } = Route.useLoaderData()
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}
Calling from Components
Use the useServerFn hook to call server functions from event handlers:
import { useServerFn } from '@tanstack/react-start'
const deletePost = createServerFn({ method: 'POST' })
.inputValidator((data: { id: string }) => data)
.handler(async ({ data }) => {
await db.delete('posts').where({ id: data.id })
return { success: true }
})
function DeleteButton({ postId }: { postId: string }) {
const deletePostFn = useServerFn(deletePost)
return (
<button onClick={() => deletePostFn({ data: { id: postId } })}>
Delete
</button>
)
}
Input Validation
Basic Validator
const greetUser = createServerFn({ method: 'GET' })
.inputValidator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })
Zod Validator
import { z } from 'zod'
const createUser = createServerFn({ method: 'POST' })
.inputValidator(
z.object({
name: z.string().min(1),
age: z.number().min(0),
}),
)
.handler(async ({ data }) => {
return `Created user: ${data.name}, age ${data.age}`
})
FormData
const submitForm = createServerFn({ method: 'POST' })
.inputValidator((data) => {
if (!(data instanceof FormData)) {
throw new Error('Expected FormData')
}
return {
name: data.get('name')?.toString() || '',
email: data.get('email')?.toString() || '',
}
})
.handler(async ({ data }) => {
return { success: true }
})
Error Handling
Errors
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message)
}
Redirects
import { redirect } from '@tanstack/react-router'
const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})
Not Found
import { notFound } from '@tanstack/react-router'
const getPost = createServerFn()
.inputValidator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})
Server Context Utilities
Access request/response details inside server function handlers:
import { createServerFn } from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
const getCachedData = createServerFn({ method: 'GET' }).handler(async () => {
const request = getRequest()
const authHeader = getRequestHeader('Authorization')
setResponseHeaders({
'Cache-Control': 'public, max-age=300',
})
setResponseStatus(200)
return fetchData()
})
Available utilities:
getRequest() — full Request object
getRequestHeader(name) — single request header
setResponseHeader(name, value) — single response header
setResponseHeaders(headers) — multiple response headers
setResponseStatus(code) — HTTP status code
File Organization
src/utils/
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe)
import { db } from '~/db'
export async function findUserById(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) })
}
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.inputValidator((data: { id: string }) => data)
.handler(async ({ data }) => {
return findUserById(data.id)
})
Static imports of server functions are safe — the build replaces implementations with RPC stubs in client bundles.
Common Mistakes
1. CRITICAL: Putting server-only code in loaders
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
},
})
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
const posts = await db.query('SELECT * FROM posts')
return { posts }
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})
2. CRITICAL: Using Next.js/Remix server patterns
'use server'
export async function getUser() { ... }
export async function getServerSideProps() { ... }
const getUser = createServerFn({ method: 'GET' })
.handler(async () => { ... })
3. HIGH: Dynamic imports for server functions
const { getUser } = await import('~/utils/users.functions')
import { getUser } from '~/utils/users.functions'
4. HIGH: Awaiting server function without calling it
createServerFn returns a function — it must be invoked with ():
const data = await getItems
const data = await getItems()
const data = await getItems({ data: { id: '1' } })
5. MEDIUM: Not using useServerFn for component calls
When calling server functions from event handlers in components, use useServerFn to get proper React integration:
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
const deletePostFn = useServerFn(deletePost)
<button onClick={() => deletePostFn({ data: { id } })}>Delete</button>
Cross-References