给AI应用写接口时,类型安全往往是最容易被忽视却又最影响开发体验的环节。传统的REST或GraphQL方案里,前端拿到的只是字符串形式的接口约定,字段改名、参数类型调整之后,编译器毫无感知,直到线上报错才发现问题。tRPC的出现改变了这一局面:它让服务端定义的路由直接成为客户端的类型来源,再配合Zod做运行时校验,可以做到从请求参数到AI模型返回结果的端到端类型安全。这篇文章带你完整走一遍基于tRPC和Zod构建AI API的全过程。

一、tRPC的核心原理:为什么它能实现端到端类型安全
tRPC的本质是一套类型层面的RPC协议。它不依赖任何代码生成工具,也不需要维护独立的Schema文件,而是直接复用TypeScript的推断能力。服务端通过procedure定义一个个可调用的接口,每个procedure的输入和输出都有明确的类型标注,客户端通过createTRPCProxyClient或React版的trpc.createClient连接后,TypeScript会沿着这个链条自动推断出每个接口的参数类型和返回值类型。
具体来说,类型传递的关键在于AppRouter这个类型。服务端导出的仅仅是类型而非运行时代码,客户端在声明时通过泛型传入AppRouter,编译器就能知道服务端有哪些路由、每个路由接受什么参数。这意味着你在前端写trpc.ai.chat.useQuery时,编辑器会直接提示messages字段是必填的、模型名称只能是几个枚举值之一,写错直接飘红。
对于AI API来说,这种机制的价值尤其明显。AI接口的返回结构通常比较复杂:可能包含流式的分片数据、token用量统计、多轮对话的上下文引用等。如果靠手写接口文档维护这些结构,前后端很快就会不一致。tRPC把唯一事实源放在服务端代码里,AI返回结构一旦调整,前端的类型立刻跟着变,重构安全感大大提升。
二、搭建项目基础:服务端路由与上下文
先准备依赖。假设前后端同处一个monorepo,安装@trpc/server、@trpc/client、zod以及@trpc/react-query。服务端入口代码如下:
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
// 公开路由与需要鉴权的路由分开
export const router = t.router;
export const publicProcedure = t.procedure;
export const appRouter = router({
ai: router({
chat: publicProcedure
.input(z.object({
messages: z.array(z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string().min(1),
})).min(1),
model: z.enum(['gpt-4o-mini', 'gpt-4o']).default('gpt-4o-mini'),
temperature: z.number().min(0).max(2).optional(),
}))
.output(z.object({
content: z.string(),
usage: z.object({
promptTokens: z.number(),
completionTokens: z.number(),
}),
}))
.query(async ({ input }) => {
// 调用AI模型并返回结构化结果
return {
content: '模拟回复内容',
usage: { promptTokens: 120, completionTokens: 45 },
};
}),
}),
});
export type AppRouter = typeof appRouter;注意最后导出的AppRouter类型,这是前端类型推断的入口。接着用fetchAdapter把tRPC挂到HTTP服务上:
import { createHTTPServer } from '@trpc/server/adapters/standalone';
createHTTPServer({
router: appRouter,
createContext() {
return {}; // 可以在这里注入用户会话、日志等上下文
},
}).listen(3000);如果需要鉴权,可以在context中解析请求头里的token,然后用t.middleware创建一个protectedProcedure,未登录请求直接在中间件层被拦截,AI调用的计费与配额逻辑也适合放在这一层统一处理。
三、Zod验证的进阶用法与错误处理
Zod不只是简单地校验类型,它还能对业务规则做精细约束。AI API里常见的场景包括:限制prompt长度防止滥用、对system消息数量做限制、给temperature设置合理区间。上面代码中已经用到了min、max、enum和default,这些约束在类型层和运行时同时生效。
校验失败时,tRPC会自动抛出BAD_REQUEST错误,错误信息里带有Zod格式化后的字段级提示。前端可以这样处理:
try {
const result = await trpc.ai.chat.query({
messages: [{ role: 'user', content: '你好' }],
});
} catch (err) {
if (err instanceof TRPCClientError) {
console.error('接口错误:', err.message);
// err.data 中包含 Zod 的详细校验信息
}
}另一个实用技巧是用z.infer从Schema反推类型,让同一份定义既做运行时校验又做编译期类型,避免两处维护。对于流式输出场景,Zod 3.23之后还提供了z.string().streaming()相关的实验支持,也可以在tRPC的subscription中逐块校验AI返回的分片数据,保证流式内容同样在类型约束之内。
四、客户端集成与流式对话调用
前端接入非常简单,以React为例:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCReact<AppRouter>();
// 在应用入口配置客户端
trpc.createClient({
links: [trpc.httpBatchLink({ url: 'http://127.0.0.1:3000' })],
});配置完成后,组件里直接用hooks调用,输入参数的类型提示完全来自服务端定义,温度设成字符串或者漏传消息数组,编辑器会立刻标红。对于AI对话这种需要流式响应的场景,把procedure换成subscription配合WebSocket链路,服务端每产生一个token就推送给客户端,前端在onNext回调中逐步渲染,体验上接近原生打字机效果。
整体来看,tRPC加Zod的组合把接口契约、运行时校验和类型推断合并成了一份代码。AI接口迭代频繁的团队采用这套方案后,前后端联调成本会显著下降,接口变更的影响也能在编译阶段就被完整捕获,值得在下一个全栈TypeScript项目中尝试。
tRPCZod验证TypeScript修改时间:2026-09-10 07:52:47