Skip to main content

server-functions

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).

Quellinformationen

Repository
TanStack/router
Letzte Quellaktivität
17. Oktober 2026 um 09:10
Erkannte Sprache von SKILL.md
Englisch
Sterne
15.249
Forks
1.913

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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' })
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen