关于 server-functions
面向 TanStack Start 开发者,介绍通过 createServerFn 实现仅在服务器运行、可从应用各处调用的类型安全 RPC。内容涵盖输入校验、认证边界、错误处理、请求上下文和缓存一致性。
主要功能
- 01创建 GET 与 POST 服务器函数
- 02从加载器、组件和事件处理器调用
- 03验证输入并处理表单数据
- 04处理错误、重定向与未找到响应
- 05访问请求和响应上下文
- 06协调持久化修改与路由缓存
适用场景
- 01将数据库访问封装在服务器端函数中
- 02验证提交到服务器的用户输入
- 03为私有数据操作添加处理器级认证
- 04修改数据后刷新路由加载数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill server-functions安装后在 Claude Code 中输入/server-functions确认已加载。
详细介绍
服务端函数
服务端函数是使用 createServerFn 创建的类型安全 RPC。它们仅在服务端运行,但可从任何位置调用——加载器、组件、钩子、事件处理程序或其他服务端函数。
CRITICAL:服务端函数是 API 端点。无论哪个路由渲染了调用它的 UI,都可以单独访问这些端点。任何会访问私有数据的服务端函数,都必须在处理程序内部(或通过中间件)强制执行身份验证。 路由的
beforeLoad用于改善用户体验,而非作为数据边界。会话/中间件模式请参见 start-core/auth-server-primitives。 CRITICAL:加载器是同构的——会在客户端和服务端运行。数据库查询、文件系统访问和机密 API 密钥必须放在createServerFn中,不能直接放在加载器中。 CRITICAL:不要使用"use server"指令、getServerSideProps或任何 Next.js/Remix 服务端模式。TanStack Start 只使用createServerFn。
基本用法
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 }
})
从加载器调用
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>
)
}
从组件调用
使用 useServerFn 钩子,从事件处理程序中调用服务端函数:
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>
)
}
缓存一致性变更
让读写操作都通过使用同一个权威数据存储的服务端函数执行。等待写入完成,然后使路由缓存失效,以便其加载器读取持久化后的结果:
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
}
持久化变更后,不要只更新组件本地状态。创建、更新、删除后以及重新加载页面后,都要验证渲染出的列表。
输入验证
基本验证器
const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })
Zod 验证器
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}`
})
输入验证不会验证输出序列化。当响应架构发生变化时,请更新数据库查询字段、服务返回值、服务端函数结果、加载器调用方和 UI。添加一项运行时测试,调用处理程序或 HTTP 边界,并断言返回的负载中包含新增字段。
FormData
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 }
})
错误处理
错误
const riskyFunction = createServerFn().handler(async () => {
throw new Error('Something went wrong!')
})
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}
重定向
import { redirect } from '@tanstack/react-router'
const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})
未找到
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
})
服务端上下文工具
在服务端函数处理程序中访问请求/响应详情:
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()
})
可用工具:
getRequest()— 完整的 Request 对象getRequestHeader(name)— 单个请求标头setResponseHeader(name, value)— 单个响应标头setResponseHeaders(headers)— 多个响应标头setResponseStatus(code)— HTTP 状态码
文件组织
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)
// 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) })
}
// 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)
})
静态导入服务端函数是安全的——构建时会将实现替换为客户端代码包中的 RPC 存根。
常见错误
1. CRITICAL:依赖路由守卫保护服务端函数
beforeLoad 重定向保护的是路由 UI,而不是数据端点。createServerFn 会暴露一个可调用的端点,攻击者可以直接访问它——完全不必加载路由。端点上的身份验证才是安全边界;路由上的身份验证只是为了改善用户体验。
// 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 } })
})
对每个需要身份验证的 createServerFn 应用 authMiddleware(或等效的处理程序内检查)。完整的会话/中间件模式请参见 start-core/auth-server-primitives,工厂组合方式请参见 start-core/middleware。
2. CRITICAL:在加载器中放入仅限服务端的代码
// 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:使用 Next.js / Remix / React Router DOM 模式
如果文件位于 src/pages/、app/layout.tsx、_app/,或导入了 react-router-dom 或 next/ 中的任何内容,那么它就是使用了错误框架的代码。TanStack Start 使用 src/routes/ + createFileRoute + createServerFn。
// 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' })
.handler(async () => { ... })
export const Route = createFileRoute('/users/$id')({
loader: ({ params }) => getUser({ data: { id: params.id } }),
component: UserPage,
})
如果你在 Agent 输出中看到 src/pages/、app/layout.tsx 或 react-router-dom,说明 Agent 正在为错误的框架生成代码。构建会失败,或者路由会在运行时冲突。
4. HIGH:动态导入服务端函数
// 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'
5. HIGH:等待服务器函数但未调用
createServerFn 会返回一个函数——必须用 () 调用它:
// 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' } })
6. CRITICAL:使用 Cache-Control: public 缓存经过身份验证的响应
Cache-Control: public, max-age=N 告诉你与用户之间的所有 CDN、代理和共享缓存,这个响应可以提供给任何人。如果响应取决于会话(用户、租户、角色),第一个用户的响应就会被缓存,并重放给下一个用户——导致跨租户数据泄露。
// WRONG — auth'd response, public cache, leaks to next user via CDN
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession() // identity-dependent
setResponseHeaders({ 'Cache-Control': 'public, max-age=300' })
return db.orders.findMany({ where: { userId: session.userId } })
})
// CORRECT — private + Vary so any cache that does store it keys by identity
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
const session = await requireSession()
setResponseHeaders({
'Cache-Control': 'private, max-age=60',
Vary: 'Cookie, Authorization',
})
return db.orders.findMany({ where: { userId: session.userId } })
})
// ALSO CORRECT — opt out entirely for sensitive data
setResponseHeaders({ 'Cache-Control': 'no-store' })
经验法则:如果处理程序读取会话/Cookie/身份验证标头,或根据身份进行分支,该响应就不是 public。默认使用 private(敏感数据则使用 no-store);只有响应内容对所有请求者都逐字节相同时,才使用 public。另请参阅 start-core/deployment,了解完整页面的 ISR/Cache-Control。
7. MEDIUM:何时使用 useServerFn 包装
当服务器函数使用 throw redirect() 或 throw notFound() 时,必须使用 useServerFn——该 Hook 会将抛出的异常接入路由器,让重定向能够实际跳转。对于仅返回数据的服务器函数(直接调用,或通过 useMutation/useQuery 调用),该 Hook 可选。
// Plain data — direct call is fine (also fine to pass to useMutation/useQuery)
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
useMutation({ mutationFn: deletePost })
// Throws redirect/notFound — MUST wrap with useServerFn so the router handles the throw
const signupFn = useServerFn(signup) // signup throws redirect on success
<button onClick={() => signupFn({ data: form })}>Sign up</button>
如有疑问,就使用 useServerFn 包装。对于普通数据函数,它不会产生任何影响;如果函数之后可能添加重定向,它就是安全的默认选择。
8. CRITICAL:在 Loader 中自行请求相对 API URL
// WRONG — this loader also runs during SSR, where a relative URL may fail
export const Route = createFileRoute('/issues')({
loader: () => fetch('/api/issues').then((response) => response.json()),
})
// CORRECT — call the server function; the client build gets an RPC stub
export const Route = createFileRoute('/issues')({
loader: () => listIssues(),
})
在仅运行于浏览器的事件处理程序中,使用相对 fetch 没问题。但它并不是通用的 Loader 数据策略。
交叉引用
- start-core/execution-model — 了解代码的运行位置
- start-core/middleware — 使用中间件组合服务器函数
- start-core/auth-server-primitives — 会话、Cookie、OAuth、CSRF、速率限制(身份验证的服务器端部分;
getCurrentUser和外部会话辅助函数应放在此处,而不是模块作用域中) @tanstack/router-core#router-core/auth-and-guards— 路由侧的 UX 守卫;数据身份验证应放在服务器函数、服务器路由或 API 端点处理程序/中间件中