@tanstack/start-client-core
Version:
Modern and scalable routing for React applications
336 lines (260 loc) • 8.58 kB
Markdown
---
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
```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 }
})
```
```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>
)
}
```
Use the `useServerFn` hook to call server functions from event handlers:
```tsx
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>
)
}
```
```tsx
const greetUser = createServerFn({ method: 'GET' })
.inputValidator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })
```
```tsx
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}`
})
```
```tsx
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 }
})
```
```tsx
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}
```
```tsx
import { redirect } from '@tanstack/react-router'
const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})
```
```tsx
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
})
```
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'
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
```text
src/utils/
├── users.functions.ts
├── users.server.ts
└── schemas.ts
```
```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' })
.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.
```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(),
})
```
```tsx
// 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 () => { ... })
```
```tsx
// 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'
```
`createServerFn` returns a function — it must be invoked with `()`:
```tsx
// 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' } })
```
When calling server functions from event handlers in components, use `useServerFn` to get proper React integration:
```tsx
// WRONG — direct call doesn't integrate with React lifecycle
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
// CORRECT — useServerFn integrates with React
const deletePostFn = useServerFn(deletePost)
<button onClick={() => deletePostFn({ data: { id } })}>Delete</button>
```
- [start-core/execution-model](../execution-model/SKILL.md) — understanding where code runs
- [start-core/middleware](../middleware/SKILL.md) — composing server functions with middleware