服务器函数(Server Functions)
什么是服务器函数?
服务器函数(Server Functions)让你定义只能在服务器上运行的逻辑,并可以从应用的任何地方调用——加载器(Loader)、组件、hooks 或其他服务器函数。它们在服务器上运行,但可以从客户端代码无缝调用。
import { createServerFn } from '@tanstack/react-start'
export const getServerTime = createServerFn().handler(async () => {
// This runs only on the server
return new Date().toISOString()
})
// Call from anywhere - components, loaders, hooks, etc.
const time = await getServerTime()服务器函数提供服务器能力(数据库访问、环境变量、文件系统),同时在网络边界上保持类型安全。
注意
服务器函数是为你的 TanStack Start 应用内部调用而设计的。它们从你的应用代码中调用很方便,Start 会处理客户端/服务端边界的序列化(Serialization)。如果你需要一个能从 Start 应用外部调用的端点,请改用服务器路由。
译者注:服务器函数到底是什么?
简单说,服务器函数是「客户端能直接调用的 RPC 接口」:客户端代码里调用它,会被编译成一次 fetch 请求发给服务器;服务器执行真正的逻辑后,把结果序列化返回。对调用方来说,它看起来就像调用一个普通的 async 函数,而且全程有 TypeScript 类型保障。构建时会自动生成稳定的函数 ID,确保客户端能定位到正确的服务端模块。
同源请求
服务器函数是你的应用内部的同源 RPC 端点。浏览器对服务器函数的请求应来自同一来源(origin),Start 会用 Fetch Metadata(Sec-Fetch-Site)、Origin 或 Referer 请求头来校验。需要支持跨域请求的公开 API 或端点,请使用服务器路由。
TanStack Start 提供了 createCsrfMiddleware() 来保护服务器函数免受跨站请求伪造(CSRF)攻击。如果你的应用没有定义 src/start.ts,Start 会自动为服务器函数安装这个中间件。如果你定义了 src/start.ts,需要显式添加该中间件:
// src/start.ts
import { createStart, createCsrfMiddleware } from '@tanstack/react-start'
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
})
export const startInstance = createStart(() => ({
requestMiddleware: [csrfMiddleware],
}))默认情况下,Origin 和 Referer 检查会与传入请求的 URL 来源进行比对。如果你的部署需要允许不同的公开来源,可以在 CSRF 中间件上配置:createCsrfMiddleware({ origin: 'https://app.example.com' })。
提示
默认情况下,不带任何这些请求头(Sec-Fetch-Site、Origin 或 Referer)的请求会被拒绝。如果你的部署会剥离这些请求头,并且你有其他层能保证服务器函数请求来自同源,可以启用 createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn', allowRequestsWithoutOriginCheck: true })。
基本用法
服务器函数用 createServerFn() 创建,可以指定 HTTP 方法:
import { createServerFn } from '@tanstack/react-start'
// GET request (default)
export const getData = createServerFn().handler(async () => {
return { message: 'Hello from server!' }
})
// POST request
export const saveData = createServerFn({ method: 'POST' }).handler(async () => {
// Server-only logic
return { success: true }
})在哪里调用服务器函数
可以从以下地方调用服务器函数:
- 路由加载器——非常适合数据获取
- 组件——配合
useServerFn()hook - 其他服务器函数——组合服务器逻辑
- 事件处理器——处理表单提交、点击等
// In a route loader
export const Route = createFileRoute('/posts')({
loader: () => getServerPosts(),
})
// In a component
function PostList() {
const getPosts = useServerFn(getServerPosts)
const { data } = useQuery({
queryKey: ['posts'],
queryFn: () => getPosts(),
})
}文件组织
对于较大的应用,可以考虑把服务端代码拆分到独立文件中。这里是一种做法:
src/utils/
├── users.functions.ts # Server function wrappers (createServerFn)
├── users.server.ts # Server-only helpers (DB queries, internal logic)
└── schemas.ts # Shared validation schemas (client-safe).functions.ts——导出createServerFn包装函数,可以从任何地方安全导入.server.ts——仅服务端代码,只在服务器函数 handler 内部导入.ts(无后缀)——客户端安全的代码(类型、schema、常量)
示例
// 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)
})静态导入是安全的
服务器函数可以在任何文件中被静态导入,包括客户端组件:
// ✅ Safe - build process handles environment shaking
import { getUser } from '~/utils/users.functions'
function UserProfile({ id }) {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => getUser({ data: { id } }),
})
}构建过程会把服务器函数的实现替换成客户端打包产物中的 RPC 桩(stub)。真正的服务器代码永远不会进入浏览器。
警告
避免对服务器函数使用动态导入:
// ❌ Can cause bundler issues
const { getUser } = await import('~/utils/users.functions')参数与校验
服务器函数接收一个单一的 data 参数。由于它们要跨越网络边界,校验能确保类型安全和运行时的正确性。
基本参数
import { createServerFn } from '@tanstack/react-start'
export const greetUser = createServerFn({ method: 'GET' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
return `Hello, ${data.name}!`
})
await greetUser({ data: { name: 'John' } })使用 Zod 校验
要获得更健壮的校验,可以使用 Zod 等 schema 库:
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const UserSchema = z.object({
name: z.string().min(1),
age: z.number().min(0),
})
export const createUser = createServerFn({ method: 'POST' })
.validator(UserSchema)
.handler(async ({ data }) => {
// data is fully typed and validated
return `Created user: ${data.name}, age ${data.age}`
})表单数据
用 FormData 处理表单提交:
export 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 }) => {
// Process form data
return { success: true }
})序列化类型检查
服务器函数的输入和输出都要跨越网络边界,因此 TypeScript 会检查它们是否可序列化:
- 校验器的输入类型必须可序列化。
POST服务器函数还允许使用FormData。 - handler 的返回类型必须可序列化。允许返回
Response对象。
这种默认行为被称为 strict(严格)模式。如果你确实需要退出这些类型层面的序列化检查,可以给 createServerFn 传 strict 选项:
// Disable input and output serialization type checks
export const looseServerFn = createServerFn({ strict: false })
.validator((data: { value: unknown }) => data)
.handler(async ({ data }) => {
return data.value
})
// Disable only input serialization type checks
export const looseInputServerFn = createServerFn({
strict: { input: false },
})
.validator((data: { value: unknown }) => data)
.handler(async () => {
return { ok: true }
})
// Disable only output serialization type checks
export const looseOutputServerFn = createServerFn({
strict: { output: false },
}).handler(async () => {
return getCustomSerializedValue()
})警告
strict: false 只是放宽了 TypeScript 的序列化检查。值在客户端和服务端之间传输时,仍然需要被运行时序列化层正确处理。除非你明确知道默认的序列化规则对某个服务器函数来说太严格了,否则请优先使用默认的 strict: true。
错误处理与重定向
服务器函数可以抛出错误、重定向(redirect)和未找到(not-found)响应,当从路由生命周期或使用 useServerFn() 的组件中调用时,这些会被自动处理。
基本错误
import { createServerFn } from '@tanstack/react-start'
export const riskyFunction = createServerFn().handler(async () => {
if (Math.random() > 0.5) {
throw new Error('Something went wrong!')
}
return { success: true }
})
// Errors are serialized to the client
try {
await riskyFunction()
} catch (error) {
console.log(error.message) // "Something went wrong!"
}重定向
使用重定向来处理认证、导航等:
import { createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
export const requireAuth = createServerFn().handler(async () => {
const user = await getCurrentUser()
if (!user) {
throw redirect({ to: '/login' })
}
return user
})未找到
对缺失的资源抛出 not-found 错误:
import { createServerFn } from '@tanstack/react-start'
import { notFound } from '@tanstack/react-router'
export const getPost = createServerFn()
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
const post = await db.findPost(data.id)
if (!post) {
throw notFound()
}
return post
})进阶主题
关于更高级的服务器函数模式与特性,请看这些专门指南:
服务器上下文与请求处理
访问请求头、cookies,以及定制响应:
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.
export const getPublicData = createServerFn({ method: 'GET' }).handler(
async () => {
setResponseHeaders(
new Headers({
// 'public' is correct ONLY when the response does not depend on identity.
// For anything tied to a session/user/tenant, see the authenticated example below.
'Cache-Control': 'public, max-age=300',
'CDN-Cache-Control': 'max-age=3600, stale-while-revalidate=600',
}),
)
setResponseStatus(200)
return fetchPublicData()
},
)Cache-Control 安全
public 会告诉你和用户之间的每个 CDN/代理:这个响应可以提供给任何人。如果 handler 读取了 session、cookie 或认证头——或者以任何方式根据身份做分支——使用 public 就会把某个用户的响应缓存起来,再重放给下一个用户(跨租户数据泄露)。认证过的响应请使用 private:
// Authenticated data — must NOT be 'public'.
export const getMyOrders = createServerFn({ method: 'GET' }).handler(
async () => {
const session = await requireSession()
setResponseHeaders(
new Headers({
// '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 db.orders.findMany({ where: { userId: session.userId } })
},
)
// For sensitive data, opt out entirely:
// setResponseHeaders(new Headers({ 'Cache-Control': 'no-store' }))可用的工具函数:
getRequest()——访问完整的 Request 对象getRequestHeader(name)——读取指定的请求头setResponseHeader(name, value)——设置单个响应头setResponseHeaders(headers)——通过 Headers 对象批量设置响应头setResponseStatus(code)——设置 HTTP 状态码
流式传输
从服务器函数向客户端流式传输带类型的数据。请看从服务器函数流式传输数据指南。
原始响应
返回 Response 对象来传输二进制数据,或自定义的 Content-Type。
渐进增强
利用 .url 属性配合 HTML 表单,在无 JavaScript 的情况下使用服务器函数。
中间件
用中间件组合服务器函数,用于认证、日志记录和共享逻辑。请看中间件指南。
在服务数据的端点上保护数据。 服务器函数是独立可访问的 API 端点,与渲染调用方 UI 的路由无关。请对每一个读写私有数据的服务器函数应用
authMiddleware或等价的手写 handler 检查。beforeLoad对路由 UX 很有用,但它不是数据边界。请看认证服务器原语。
静态服务器函数
在构建时缓存服务器函数的结果,用于静态生成。请看静态服务器函数。
服务器组件
服务器函数可以返回 Server Components——由服务器渲染、客户端可以组合的 React 组件。请看服务器组件。
请求取消
用 AbortSignal 处理长时间运行操作的请求取消。
生产构建的函数 ID 生成
服务器函数底层通过一个生成出来的、稳定的函数 ID 寻址。这些 ID 被嵌入到客户端/SSR 构建中,服务器在运行时用它定位并导入正确的模块。
默认情况下,ID 是同一 seed 的 SHA256 哈希,以保持打包产物紧凑并避免泄漏文件路径。
如果两个服务器函数最终得到相同的 ID(包括使用自定义生成器的情况),系统会通过追加递增后缀(如 _1、_2)来去重。
自定义:
你可以在配置 TanStack Start 构建工具插件时,通过提供 generateFunctionId 函数来定制生产构建的函数 ID 生成。
最好使用确定性输入(filename + functionName),这样 ID 才能在多次构建之间保持稳定。
请注意,这个定制是实验性的,将来可能发生变化。
示例:
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
tanstackStart({
serverFns: {
generateFunctionId: ({ filename, functionName }) => {
return crypto
.createHash('sha1')
.update(`${filename}--${functionName}`)
.digest('hex')
},
},
}),
],
})注意: 服务器函数使用一套编译过程,把服务器代码从客户端打包产物中抽取出来,同时保持无缝的调用模式。在客户端,调用会变成对服务器的
fetch请求。