规范使用 TanStack Start Server Functions

接着上篇聊路由层面防线的话题，咱们继续顺着 TanStack Start 的请求链路往深处走：这次聊聊 Server Functions。当项目脱离了“Demo 期”，真正开始承载复杂业务逻辑时，首当其冲要面临考验的就是这一层。刚上手时，我们很容易顺手就把数据库查询、数据拼装甚至权限判断全都糊在一个 createServerFn 里。起初这会让你觉得全栈开发“效率奇高”，但很快，你就会在代码库里迷失。在这个博客项目里，我对 createServerFn 的定位极其克制：它是纯粹的接口层，绝不是业务层。它只负责这几件苦力活：声明 HTTP Method 编排中间件拦截器校验入参格式把合法请求恭敬地递交给后方的 Service 至于真正的业务细节？它一行都不应该有。接口层解剖：评论创建逻辑评论创建功能，是最能体现 Server Functions 价值的场景。 export const createCommentFn = createServerFn({ method: "POST", }) .middleware([ createRateLimitMiddleware({ capacity: 10, interval: "1m", key: "comments:create", }), turnstileMiddleware, authMiddleware, ]) .inputValidator(CreateCommentInputSchema) .handler( async ({ data, context }) => await CommentService.createComment(context, data), ); 你看，仅仅这十几行代码，其实已经把一个生产级接口要求的东西拉满了：这是写操作，所以锁定了 POST 串联了限流兜底（基于 Durable Objects 分布式限流）串联了 Turnstile 人机验证串联了必须登录的 Auth 校验 inputValidator 对抗所有的脏输入最终，把干净的 data 丢给 CommentService 整个结构非常漂亮。接口层有明确的宏观职责，但它自己绝对不会长出不可控的业务逻辑分支。中间件编排：告别面条代码如果不利用中间件，我们在传统的 API Controller 里得怎么写？恐怕得一遍遍地手写：先连数据库，再验 Session，再查验证码，再算限流额度…… 相比之下，Server Functions 利用中间件组合的写法简直是降维打击。比如，普通的查询接口和刚才的写接口，权限要求就截然不同： export const getRootCommentsByPostIdFn = createServerFn() .middleware([sessionMiddleware]) .inputValidator(GetCommentsByPostIdInputSchema) .handler(async ({ data, context }) => { const session = context.session; return await CommentService.getRootCommentsByPostId(context, { ...data, viewerId: session?.user.id, }); }); 读接口只需要挂载个轻量的 sessionMiddleware 就够了。一旦底层中间件体系搭建完毕，新写一个接口就如同搭积木一样简单。强类型的文件上传：FormData处理全栈框架处理起 JSON 往往很丝滑，但一碰到文件上传等 FormData 场景，接口层就开始群魔乱舞。在 TanStack Start 里，处理文件导入其实和处理 JSON 没什么心智负担： // 强制校验输入必须是原生的 FormData 实例 const UploadForImportInputSchema = z.instanceof(FormData); export const uploadForImportFn = createServerFn({ method: "POST", }) .middleware([adminMiddleware]) .inputValidator(UploadForImportInputSchema) .handler(async ({ data: formData, context }) => { // 过滤出真正的 File 对象 const files = formData .getAll("file") .filter((f): f is File => f instanceof File); return await ImportExportService.startImport(context, files); }); 依然保持着强类型约束：请求方法明确权限隔离明确入参对象收束明确这里顺带提一嘴，不管是在处理表单还是普通校验阶段，遇到异常一定要抛出类型安全的 Error，前两天总结的那篇 用 Result 类型处理全栈错误 同样适用于 Server Functions 的 handler 返回层。 Service 纯粹化标签系统和后台索引重建的接口，最能展现这种“强行压薄”的爽感： // 创建标签接口 export const createTagFn = createServerFn({ method: "POST", }) .middleware([adminMiddleware]) .inputValidator(CreateTagInputSchema) .handler(({ data, context }) => TagService.createTag(context, data)); // 触发搜索索引全量重建接口 export const buildSearchIndexFn = createServerFn({ method: "POST" }) .middleware([adminMiddleware]) .handler(({ context }) => SearchService.rebuildIndex(context)); 没有废话。如果你用传统的框架去写 API 路由文件（比如 Next.js 的 App Router Route Handlers 或者原始的 Express），巨大的空白区域很容易诱惑你往里面塞几行“临时”的 SQL 查询或者胶水逻辑。而 createServerFn 这种链式调用的写法，直接在视觉和语法上锁死了你乱写代码的空间。它会“逼着”你老老实实地回 Service 模块里去设计聚合根和领域方法。 Server Functions 的边界我不会把 Server Functions 吹成无所不能的银弹。它的主场在同构全栈体系内：它让前后端的互相调用几乎没有摩擦，类型直接打通，省去了手写 Swagger 和 Fetch 的冗长痛点。但它也有无法覆盖的盲区：如果你需要开放正规的 RESTful / GraphQL API 给第三方客户端接入，光靠 Server Functions 肯定不踏实，还是得外挂一层真正的外部 API 层。对于脱离了框架 HTTP 请求生命周期的任务，比如 Cloudflare Workflows、Queues 的消费者，这套逻辑自然也管不着。这也是为什么我的博客架构里，除了这套对内的 Server Functions，针对特殊场景还分别暴露了纯粹的 Hono 接口、MCP Entrypoint 和 Workflow 的触发器。没有任何一种工具有资格包揽全部，看菜下饭就好。总结一句话：永远把 Server Functions 当接线员，不要当包工头。它存在的意义是把网上的“荒蛮报文”，优雅地洗出：靠谱的 HTTP Method 层层清洗过的 Middleware Context 格式极其确定的 Input Validator 然后呢？然后就交给后端 Service 干活去吧。