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 usescreateServerFnexclusively.
Basic Usage
import { createServerFn } from "@tanstack/react-start"
// GET (default)
const getData = createServerFn().handler(async () => {
return { message: "Hello from server!" }
})
// POST
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 => 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) // "Something went wrong!"
}
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 objectgetRequestHeader(name)— single request headersetResponseHeader(name, value)— single response headersetResponseHeaders(headers)— multiple response headerssetResponseStatus(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)
// users.server.ts — server-only helpers
import { db } from "~/db"
export async function findUserById(id: string) {
return db.query.users.findFirst({ where: eq(users.id, id) })
}
// users.functions.ts — server functions
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
// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
export const Route = createFileRoute("/posts")({
loader: async () => {
const posts = await db.query("SELECT * FROM posts")
return { posts }
},
})
// CORRECT — use createServerFn for server-only logic
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
// WRONG — "use server" is a React directive, not used in TanStack Start
'use server'
export async function getUser() { ... }
// WRONG — getServerSideProps is Next.js
export async function getServerSideProps() { ... }
// CORRECT — TanStack Start uses createServerFn
const getUser = createServerFn({ method: 'GET' })
.handler(async () => { ... })
3. HIGH: Dynamic imports for server functions
// WRONG — can cause bundler issues
const { getUser } = await import("~/utils/users.functions")
// CORRECT — static imports are safe, build handles environment shaking
import { getUser } from "~/utils/users.functions"
4. HIGH: Awaiting server function without calling it
createServerFn returns a function — it must be invoked with ():
// WRONG — getItems is a function, not a Promise
const data = await getItems
// CORRECT — call the function
const data = await getItems()
// With validated input
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:
// WRONG — direct call doesn't integrate with React lifecycle
<button => deletePost({ data: { id } })}>Delete</button>
// CORRECT — useServerFn integrates with React
const deletePostFn = useServerFn(deletePost)
<button => deletePostFn({ data: { id } })}>Delete</button>
Cross-References
- start-core/execution-model — understanding where code runs
- start-core/middleware — composing server functions with middleware