TanStack Start 中文文档
服务器与执行

服务器函数(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)、OriginReferer 请求头来校验。需要支持跨域请求的公开 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],
}))

默认情况下,OriginReferer 检查会与传入请求的 URL 来源进行比对。如果你的部署需要允许不同的公开来源,可以在 CSRF 中间件上配置:createCsrfMiddleware({ origin: 'https://app.example.com' })

提示

默认情况下,不带任何这些请求头(Sec-Fetch-SiteOriginReferer)的请求会被拒绝。如果你的部署会剥离这些请求头,并且你有其他层能保证服务器函数请求来自同源,可以启用 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(严格)模式。如果你确实需要退出这些类型层面的序列化检查,可以给 createServerFnstrict 选项:

// 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 才能在多次构建之间保持稳定。

请注意,这个定制是实验性的,将来可能发生变化。

示例:

vite.config.ts
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 请求。

On this page