tRPC 端到端类型安全 API¶
一句话概述:tRPC 让你在 TypeScript 全栈项目中实现端到端类型安全——改了后端函数签名,前端调用处自动报错提示,不需要写 Schema、不需要代码生成、不需要 REST/GraphQL。
核心知识点¶
| 概念 | 白话解释 |
|---|---|
| 端到端类型安全 | 后端改了返回类型,前端 TypeScript 编译时就能发现错误 |
| Procedure | tRPC 的"接口",类似 REST 的 endpoint,分 query/mutation/subscription |
| Router | 路由器,把多个 procedure 组织在一起 |
| query | 查询数据(类似 GET 请求) |
| mutation | 修改数据(类似 POST/PUT/DELETE 请求) |
| subscription | 订阅实时数据(SSE 方式) |
| Context | 上下文,在每个请求中传递认证信息、数据库连接等 |
| Middleware | 中间件,在 procedure 执行前做验证、日志等 |
安装配置¶
Next.js App Router(最流行的用法)¶
# 创建 Next.js 项目
npx create-next-app@latest my-app --typescript --tailwind --app # 创建项目
cd my-app
# 安装 tRPC 相关包
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next # tRPC 核心包
npm install @tanstack/react-query # React Query,tRPC 的前端请求层
npm install zod # Zod 验证库,用于输入参数校验
npm install superjson # JSON 序列化增强,支持 Date 等类型
项目结构¶
my-app/
├── src/
│ ├── server/
│ │ ├── trpc.ts # tRPC 初始化(上下文、中间件)
│ │ ├── routers/
│ │ │ ├── _app.ts # 根 Router(合并所有子路由)
│ │ │ ├── user.ts # 用户相关接口
│ │ │ └── post.ts # 文章相关接口
│ │ └── context.ts # 请求上下文定义
│ ├── app/
│ │ └── api/trpc/[trpc]/route.ts # API 路由入口
│ └── lib/
│ └── trpc.ts # 前端 tRPC Client
后端配置¶
// src/server/trpc.ts - tRPC 初始化
import { initTRPC, TRPCError } from '@trpc/server' // 导入 tRPC 初始化函数
import superjson from 'superjson' // 导入 JSON 序列化工具
import { z } from 'zod' // 导入 Zod 验证库
// 定义上下文类型(每个请求都能拿到的信息)
export type Context = {
userId?: string // 当前登录用户 ID(可选,未登录就没有)
db: any // 数据库连接
}
// 初始化 tRPC
const t = initTRPC.context<Context>().create({
transformer: superjson, // 用 superjson 处理 Date、Map 等特殊类型
})
// 导出可复用的构建块
export const router = t.router // 路由器构建函数
export const publicProcedure = t.procedure // 公开接口(无需登录)
export const middleware = t.middleware // 中间件
// 认证中间件:检查用户是否登录
const isAuthed = middleware(async ({ ctx, next }) => {
if (!ctx.userId) { // 没有 userId 说明未登录
throw new TRPCError({ code: 'UNAUTHORIZED', message: '请先登录' }) // 抛出未授权错误
}
return next({
ctx: { userId: ctx.userId }, // 把 userId 传给下一步
})
})
// 需要登录才能访问的接口
export const protectedProcedure = publicProcedure.use(isAuthed) // 套上认证中间件
定义路由¶
// src/server/routers/user.ts - 用户路由
import { z } from 'zod' // 导入验证库
import { router, publicProcedure, protectedProcedure } from '../trpc' // 导入 tRPC 构建块
export const userRouter = router({
// 查询接口:获取用户列表
list: publicProcedure // 公开接口,不需要登录
.query(async ({ ctx }) => { // query = 查询操作
// ctx.db 是从上下文拿到的数据库连接
return [ // 返回用户列表
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' },
]
}),
// 查询接口:根据 ID 获取用户
getById: publicProcedure
.input(z.object({ // 定义输入参数的验证规则
id: z.number().positive(), // id 必须是正整数
}))
.query(async ({ input, ctx }) => {
// input 的类型自动推断为 { id: number }
return { id: input.id, name: '张三' }
}),
// 修改接口:创建用户
create: protectedProcedure // 需要登录才能调用
.input(z.object({
name: z.string().min(2, '名字至少2个字'), // 字符串,最少2个字符
email: z.string().email('邮箱格式不对'), // 必须是邮箱格式
age: z.number().min(0).max(150).optional(), // 可选,0-150 之间
}))
.mutation(async ({ input, ctx }) => { // mutation = 修改操作
console.log('创建者:', ctx.userId) // 从上下文拿到当前用户
// 实际项目中这里写数据库操作
return { id: Date.now(), ...input } // 返回创建的用户
}),
// 修改接口:更新用户
update: protectedProcedure
.input(z.object({
id: z.number(),
name: z.string().optional(),
email: z.string().email().optional(),
}))
.mutation(async ({ input }) => {
return { ...input, updatedAt: new Date() }
}),
})
// src/server/routers/_app.ts - 根路由(合并所有子路由)
import { router } from '../trpc'
import { userRouter } from './user'
export const appRouter = router({
user: userRouter, // 把用户路由挂到 user 命名空间下
// post: postRouter, // 以后加更多路由
})
export type AppRouter = typeof appRouter // 导出类型,前端用这个实现类型安全
API 路由入口¶
// src/app/api/trpc/[trpc]/route.ts - Next.js API 路由
import { fetchRequestHandler } from '@trpc/server/adapters/fetch' // 导入 fetch 适配器
import { appRouter } from '@/server/routers/_app'
const handler = (req: Request) => {
return fetchRequestHandler({
endpoint: '/api/trpc', // API 路径前缀
req, // 请求对象
router: appRouter, // 根路由
createContext: async () => ({ // 创建上下文
userId: 'user-123', // 实际项目从 session/JWT 获取
db: null, // 实际项目传数据库连接
}),
})
}
export { handler as GET, handler as POST } // 同时处理 GET 和 POST
前端客户端配置¶
// src/lib/trpc.ts - 前端 tRPC 客户端
import { createTRPCReact } from '@trpc/react-query' // 导入 React tRPC
import type { AppRouter } from '@/server/routers/_app' // 导入后端类型(只导入类型!)
export const trpc = createTRPCReact<AppRouter>() // 创建类型安全的客户端
基本使用¶
前端调用接口¶
'use client' // Next.js 客户端组件标记
import { trpc } from '@/lib/trpc' // 导入 tRPC 客户端
export function UserList() {
// 查询用户列表 - 自动有类型提示!
const { data: users, isLoading, error } = trpc.user.list.useQuery()
// data 的类型自动推断为后端 query 的返回类型
// 根据 ID 查询
const { data: user } = trpc.user.getById.useQuery({ id: 1 })
// input 参数也有类型校验
// 创建用户 mutation
const createUser = trpc.user.create.useMutation({
onSuccess: (data) => { // data 类型自动推断
console.log('创建成功:', data.name)
},
onError: (error) => {
console.error('创建失败:', error.message)
},
})
if (isLoading) return <div>加载中...</div> // 加载状态
if (error) return <div>出错了: {error.message}</div> // 错误状态
return (
<div>
<h1>用户列表</h1>
{users?.map((user) => ( // 遍历用户
<div key={user.id}>
{user.name} - {user.email} {/* 所有字段都有类型提示 */}
</div>
))}
<button onClick={() => {
createUser.mutate({ // 调用 mutation
name: '新用户', // 输入参数有类型校验
email: 'new@example.com',
})
}}>
创建用户
</button>
</div>
)
}
高级用法¶
错误处理¶
// 后端:自定义错误
import { TRPCError } from '@trpc/server'
export const postRouter = router({
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
const post = await findPost(input.id)
if (!post) {
throw new TRPCError({ // 抛出 tRPC 错误
code: 'NOT_FOUND', // 错误码
message: `找不到 ID 为 ${input.id} 的文章`, // 错误信息
})
}
return post
}),
})
// 前端:捕获错误
const { data, error } = trpc.post.getById.useQuery(
{ id: 999 },
{
retry: false, // 不自动重试
onError: (err) => {
if (err.data?.code === 'NOT_FOUND') { // 根据错误码处理
alert('文章不存在')
}
},
}
)
实时订阅(SSE)¶
// 后端:定义订阅
import { observable } from '@trpc/server/observable'
export const chatRouter = router({
onMessage: publicProcedure
.subscription(({ ctx }) => { // subscription 类型的 procedure
return observable<{ text: string; timestamp: Date }>((emit) => {
const interval = setInterval(() => { // 每秒发送一条消息
emit.next({ // 向客户端推送数据
text: `消息 ${Date.now()}`,
timestamp: new Date(),
})
}, 1000)
return () => clearInterval(interval) // 清理函数,取消订阅时调用
})
}),
})
// 前端:订阅消息
trpc.chat.onMessage.useSubscription(undefined, { // undefined 表示无输入参数
onData: (message) => { // 收到数据时的回调
console.log('收到消息:', message.text)
},
})
文件上传¶
// 后端:处理文件上传
export const fileRouter = router({
upload: protectedProcedure
.input(z.object({
fileName: z.string(),
fileType: z.string(),
fileSize: z.number().max(10 * 1024 * 1024, '文件不能超过 10MB'), // 限制大小
}))
.mutation(async ({ input }) => {
// 生成预签名 URL(实际项目用 S3/R2 等)
const uploadUrl = `https://storage.example.com/upload/${input.fileName}`
return { uploadUrl } // 返回上传地址给前端
}),
})
批量请求优化¶
// 前端配置 - 启用请求批量合并
import { httpBatchLink } from '@trpc/client' // 批量请求链接
const trpcClient = trpc.createClient({
links: [
httpBatchLink({ // 多个同时发出的请求会合并成一个 HTTP 请求
url: '/api/trpc',
maxURLLength: 2083, // URL 太长时自动切换为 POST
}),
],
})
常见报错¶
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
TRPCClientError: UNAUTHORIZED | 未登录就调用了受保护接口 | 确保登录后再调用 protectedProcedure |
Input validation failed | 输入参数格式不对 | 检查 Zod 验证规则和传入的参数 |
Type 'xxx' is not assignable | 前后端类型不匹配 | 确保前端导入了正确的 AppRouter 类型 |
Cannot find module '@trpc/server' | 包没安装 | npm install @trpc/server @trpc/client |
| 调用没有类型提示 | AppRouter 类型没正确导出 | 检查 _app.ts 中 export type AppRouter |
INTERNAL_SERVER_ERROR | 后端代码抛出异常 | 检查后端 procedure 的实现逻辑 |
速查表¶
// tRPC 构建块
router({}) // 创建路由器
publicProcedure // 公开接口
protectedProcedure // 需要认证的接口
.input(zodSchema) // 定义输入参数验证
.output(zodSchema) // 定义输出类型验证
.query(handler) // 查询(GET)
.mutation(handler) // 修改(POST/PUT/DELETE)
.subscription(handler) // 实时订阅
// 前端 React Hooks
trpc.xxx.useQuery() // 查询数据
trpc.xxx.useMutation() // 修改数据
trpc.xxx.useSubscription() // 订阅实时数据
trpc.xxx.useInfiniteQuery() // 无限滚动分页
// TRPCError 错误码
'BAD_REQUEST' // 400 请求参数错误
'UNAUTHORIZED' // 401 未授权
'FORBIDDEN' // 403 禁止访问
'NOT_FOUND' // 404 未找到
'CONFLICT' // 409 冲突
'TOO_MANY_REQUESTS' // 429 请求太频繁
'INTERNAL_SERVER_ERROR' // 500 服务器内部错误
// Zod 常用验证
z.string() // 字符串
z.number() // 数字
z.boolean() // 布尔
z.object({}) // 对象
z.array() // 数组
z.enum(['a','b']) // 枚举
z.optional() // 可选
z.nullable() // 可为null
.min() .max() // 最小/最大值
.email() .url() // 邮箱/URL格式
参考:tRPC 官网 | tRPC GitHub | tRPC v11 文档