| 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