- name
- server-functions
- description
- createServerFn (GET/POST), validator (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).
- metadata
- {"type":"sub-skill","library":"tanstack-start","library_version":"1.170.14"}
- 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**: Server functions are API endpoints. They are reachable independently of whichever route renders the calling UI. **Auth must be enforced inside the handler (or via middleware) for any server function that touches private data.** Route `beforeLoad` is UX, not the data boundary. See [start-core/auth-server-primitives](../auth-server-primitives/SKILL.md) for the session/middleware pattern.
> **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
```tsx
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
```tsx
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:
```tsx
import { useServerFn } from '@tanstack/react-start'
const deletePost = createServerFn({ method: 'POST' })
.validator((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>
)
}
```
## Cache-Coherent Mutations
Keep reads and writes behind server functions that use the same authoritative store. Await the write, then invalidate the route cache so its loader reads the persisted result:
```tsx
const listIssues = createServerFn({ method: 'GET' }).handler(() => {
return db.issues.findMany()
})
const createIssue = createServerFn({ method: 'POST' })
.validator((data: unknown) => {
if (
typeof data !== 'object' ||
data === null ||
!('title' in data) ||
typeof data.title !== 'string' ||
data.title.trim().length === 0
) {
throw new Error('Title is required')
}
return { title: data.title.trim() }
})
.handler(async ({ data }) => {
return db.issues.create({ data })
})
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
component: IssuesPage,
})
function IssuesPage() {
const router = useRouter()
const createIssueFn = useServerFn(createIssue)
const handleCreate = async (title: string) => {
await createIssueFn({ data: { title } })
await router.invalidate({ sync: true })
}
// render Route.useLoaderData() and call handleCreate from the form
}
```
Do not update only local component state after a persistent mutation. Verify the rendered list after create, update, delete, and a fresh page load.
## Input Validation
### Basic Validator
```tsx
const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })
```
### Zod Validator
```tsx
import { z } from 'zod'
const createUser = createServerFn({ method: 'POST' })
.validator(
z.object({
name: z.string().min(1),
age: z.number().min(0),
}),
)
.handler(async ({ data }) => {
return `Created user: ${data.name}, age ${data.age}`
})
```
Input validation does not validate output serialization. When a response schema changes, update the database selection, service return value, server-function result, loader consumer, and UI. Add a runtime test that calls the handler or HTTP boundary and asserts the new field in the returned payload.
### FormData
```tsx
const submitForm = createServerFn({ method: 'POST' })
.validator((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
```tsx
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}
```
### Redirects
```tsx
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
```tsx
import { notFound } from '@tanstack/react-router'
const getPost = createServerFn()
.validator((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:
```tsx
import { createServerFn } from '@tanstack/react-start'
import {
getRequest,
getRequestHeader,
setResponseHeaders,
setResponseStatus,
} from '@tanstack/react-start/server'
// Public, non-personalized data — safe to cache shared across users.
const getPublicData = createServerFn({ method: 'GET' }).handler(async () => {
setResponseHeaders({
// 'public' is correct ONLY when the response does not depend on identity.
// For anything tied to a session/user/tenant, use 'private' or 'no-store'.
'Cache-Control': 'public, max-age=300',
})
setResponseStatus(200)
return fetchPublicData()
})
// Authenticated data — must NOT be 'public'.
const getMyData = createServerFn({ method: 'GET' }).handler(async () => {
const authHeader = getRequestHeader('Authorization')
// ... auth check ...
setResponseHeaders({
// 'private' = only the user-agent may cache. Vary by Cookie/Authorization
// so any intermediary that does cache keys by identity, not URL alone.
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return fetchPersonalizedData()
})
```
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
```text
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)
```
```tsx
// 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) })
}
```
```tsx
// users.functions.ts — server functions
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'
export const getUser = createServerFn({ method: 'GET' })
.validator((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: Relying on a route guard to protect a server function
A `beforeLoad` redirect protects the **route's UI**, not the **data endpoint**. `createServerFn` exposes a callable endpoint that an attacker can hit directly — no need to load the route at all. Auth on the endpoint is the security boundary; auth on the route is UX.
```tsx
// WRONG — the route guard doesn't reach the handler
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
return db.orders.findMany() // ← anyone can call the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
loader: () => getMyOrders(),
})
// CORRECT — auth enforced on the handler itself
const getMyOrders = createServerFn({ method: 'GET' })
.middleware([authMiddleware])
.handler(async ({ context }) => {
return db.orders.findMany({ where: { userId: context.session.userId } })
})
```
Apply `authMiddleware` (or an equivalent in-handler check) to **every** `createServerFn` that needs auth. See [start-core/auth-server-primitives](../auth-server-primitives/SKILL.md) for the full session/middleware pattern and [start-core/middleware](../middleware/SKILL.md) for composing the factory.
### 2. CRITICAL: Putting server-only code in loaders
```tsx
// 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(),
})
```
### 3. CRITICAL: Using Next.js / Remix / React Router DOM patterns
If the file lives at `src/pages/`, `app/layout.tsx`, `_app/`, or imports anything from `react-router-dom` or `next/`, it is wrong-framework code. TanStack Start uses `src/routes/` + `createFileRoute` + `createServerFn`.
```tsx
// WRONG — "use server" is a React directive, not used in TanStack Start
'use server'
export async function getUser() { ... }
// WRONG — getServerSideProps is Next.js Pages Router
export async function getServerSideProps() { ... }
// WRONG — Next.js App Router server component data fetching
export default async function Page() {
const data = await fetch(...).then(r => r.json())
return <div>{data}</div>
}
// WRONG — Remix
export async function loader({ request }) { ... }
export async function action({ request }) { ... }
// WRONG — react-router-dom (a different library)
import { Link, useNavigate } from 'react-router-dom'
// CORRECT — TanStack Start
import { createServerFn } from '@tanstack/react-start'
import { Link, useNavigate, createFileRoute } from '@tanstack/react-router'
const getUser = createServerFn({ method: 'GET' })
View on GitHub