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

소스 정보

저장소
TanStack/router
최근 소스 활동
2026년 10월 12일 22:01
감지된 SKILL.md 언어
영어
스타
15,202
포크
1,900

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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' })
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기