tRPC 端到端类型安全
tRPC 让你用写函数的方式写 API——后端定义 procedure,前端直接调用,TypeScript 自动推导参数和返回值类型,零 Schema 手动同步。配合 Next.js App Router,它是构建类型安全全栈应用的终极方案。
1. 为什么选 tRPC
1.1 传统 API 的类型断层
传统 REST API 的类型断层:
后端定义 API:
POST /api/chat body: { title: string, model: string } → { id: string, ... }
前端调用:
const res = await fetch('/api/chat', { body: JSON.stringify(data) })
const chat = await res.json() // 类型是 any!
问题:
├── 前端不知道请求体的类型
├── 前端不知道响应的类型
├── API 改了字段名,前端不会报错(运行时才发现)
└── 手动维护类型定义(容易过时)
1.2 tRPC 的解决方案
tRPC 的类型安全:
后端定义 procedure:
chat.create = t.procedure
.input(z.object({ title: z.string(), model: z.string() }))
.mutation(async ({ input }) => { ... })
前端调用:
const chat = await trpc.chat.create.mutate({ title: '新对话', model: 'gpt-4o' })
// ↑ 类型自动推导 ↑ 参数类型检查
结果:
├── 参数类型自动检查(title 必须是 string)
├── 返回值类型自动推导(chat 的类型自动获取)
├── API 改了字段名 → 前端立刻报编译错误
└── 零手动类型维护
2. 基础配置
2.1 安装
pnpm add @trpc/server @trpc/client @trpc/react-query @trpc/next
pnpm add @tanstack/react-query
pnpm add zod superjson2.2 tRPC 初始化
// lib/trpc/init.ts
import { initTRPC, TRPCError } from '@trpc/server'
import superjson from 'superjson'
import { auth } from '@/lib/auth/config'
export const createTRPCContext = async () => {
const session = await auth()
return { session }
}
export type Context = Awaited<ReturnType<typeof createTRPCContext>>
const t = initTRPC.context<Context>().create({
transformer: superjson,
})
export const router = t.router
export const publicProcedure = t.procedure
// 需要登录的 procedure
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: '请先登录' })
}
return next({
ctx: { ...ctx, session: ctx.session },
})
})2.3 Route Handler 入口
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/lib/trpc/router'
import { createTRPCContext } from '@/lib/trpc/init'
const handler = (request: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req: request,
router: appRouter,
createContext: createTRPCContext,
})
export { handler as GET, handler as POST }3. Router 定义
3.1 基础 Router
// lib/trpc/routers/chat.ts
import { z } from 'zod'
import { router, protectedProcedure } from '../init'
export const chatRouter = router({
// 查询列表
list: protectedProcedure
.input(z.object({
model: z.string().optional(),
limit: z.number().min(1).max(100).default(50),
}))
.query(async ({ ctx, input }) => {
return db.query.chats.findMany({
where: and(
eq(chats.userId, ctx.session.user.id),
input.model ? eq(chats.model, input.model) : undefined,
),
limit: input.limit,
orderBy: desc(chats.updatedAt),
})
}),
// 查询单个
byId: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const chat = await db.query.chats.findFirst({
where: and(
eq(chats.id, input.id),
eq(chats.userId, ctx.session.user.id),
),
with: { messages: true },
})
if (!chat) throw new TRPCError({ code: 'NOT_FOUND' })
return chat
}),
// 创建
create: protectedProcedure
.input(z.object({
title: z.string().min(1).max(100),
model: z.enum(['gpt-4o', 'claude-3.5-sonnet', 'gemini-pro']),
}))
.mutation(async ({ ctx, input }) => {
const [chat] = await db.insert(chats).values({
...input,
userId: ctx.session.user.id,
}).returning()
return chat
}),
// 删除
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
await db.delete(chats).where(
and(eq(chats.id, input.id), eq(chats.userId, ctx.session.user.id)),
)
}),
})3.2 合并 Router
// lib/trpc/router.ts
import { router } from './init'
import { chatRouter } from './routers/chat'
import { userRouter } from './routers/user'
import { billingRouter } from './routers/billing'
import { knowledgeRouter } from './routers/knowledge'
export const appRouter = router({
chat: chatRouter,
user: userRouter,
billing: billingRouter,
knowledge: knowledgeRouter,
})
export type AppRouter = typeof appRouter4. 客户端调用
4.1 React Query 集成
// lib/trpc/client.ts
'use client'
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './router'
export const trpc = createTRPCReact<AppRouter>()// lib/trpc/provider.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { useState } from 'react'
import superjson from 'superjson'
import { trpc } from './client'
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: { staleTime: 60 * 1000, retry: 1 },
},
}))
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
transformer: superjson,
}),
],
}),
)
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
)
}4.2 在组件中使用
// components/chat-list.tsx
'use client'
import { trpc } from '@/lib/trpc/client'
export function ChatList() {
// 类型完全自动推导!
const { data: chats, isLoading } = trpc.chat.list.useQuery({
model: 'gpt-4o',
limit: 20,
})
// chats 的类型自动推导为 Chat[] | undefined
const createChat = trpc.chat.create.useMutation({
onSuccess: () => {
// 自动重新获取列表
utils.chat.list.invalidate()
},
})
const utils = trpc.useUtils()
if (isLoading) return <Skeleton />
return (
<div>
<button onClick={() => createChat.mutate({ title: '新对话', model: 'gpt-4o' })}>
新建
</button>
{chats?.map(chat => (
<div key={chat.id}>{chat.title}</div>
))}
</div>
)
}4.3 服务端调用(Server Component)
// lib/trpc/server.ts
import { createTRPCContext } from './init'
import { appRouter } from './router'
export const serverTrpc = appRouter.createCaller(await createTRPCContext())// app/(dashboard)/chat/page.tsx (Server Component)
import { serverTrpc } from '@/lib/trpc/server'
export default async function ChatPage() {
const chats = await serverTrpc.chat.list({ limit: 50 })
// 类型安全!返回值类型自动推导
return <ChatList initialChats={chats} />
}5. 中间件管道
5.1 多层中间件
// lib/trpc/init.ts
// 基础认证
const authMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({ ctx: { ...ctx, user: ctx.session.user } })
})
// 管理员权限
const adminMiddleware = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user || ctx.session.user.role !== 'admin') {
throw new TRPCError({ code: 'FORBIDDEN', message: '需要管理员权限' })
}
return next({ ctx })
})
// 速率限制
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
const ip = ctx.session?.user?.id ?? 'anonymous'
const isAllowed = await checkRateLimit(ip)
if (!isAllowed) {
throw new TRPCError({ code: 'TOO_MANY_REQUESTS' })
}
return next({ ctx })
})
export const protectedProcedure = t.procedure.use(authMiddleware)
export const adminProcedure = t.procedure.use(authMiddleware).use(adminMiddleware)
export const rateLimitedProcedure = t.procedure.use(rateLimitMiddleware)5.2 日志中间件
const logMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now()
const result = await next()
const duration = Date.now() - start
console.log(`tRPC ${type} ${path} — ${duration}ms`)
return result
})6. tRPC vs Server Actions vs Route Handlers
| 维度 | tRPC | Server Actions | Route Handlers |
|---|---|---|---|
| 类型安全 | ✅ 端到端自动 | ✅ 端到端自动 | ⚠️ 手动定义 |
| 客户端缓存 | ✅ React Query | ❌ | ❌ |
| 批量请求 | ✅ httpBatchLink | ❌ | ❌ |
| 中间件 | ✅ 管道式 | ⚠️ 手动 | ⚠️ 手动 |
| 表单集成 | ❌ | ✅ <form action> | ❌ |
| SSE 流式 | ⚠️ 实验性 | ❌ | ✅ |
| 外部 API 调用 | ❌ | ❌ | ✅ |
| 学习成本 | 中 | 低 | 低 |
推荐组合:
- 查询(读取) → tRPC(类型安全 + 缓存 + 批量)
- 变更(表单提交) → Server Actions(渐进增强 + 简洁)
- 流式/Webhook → Route Handlers
本章小结
- tRPC 提供后端到前端的端到端类型安全,修改 API 自动触发编译错误
- Router 结构:procedure 定义 input/output → 合并到 appRouter → 通过 Route Handler 暴露
- 客户端调用:
trpc.xxx.useQuery()/useMutation()自动推导类型 - 服务端调用:
createCaller在 Server Component 中直接调用 - 中间件管道:认证 → 授权 → 速率限制 → 日志,层层递进
- 组合策略:tRPC 负责查询,Server Actions 负责表单变更,Route Handlers 负责流式和 Webhook
至此,第三篇「渲染模式与数据获取」全部完成。下一篇将深入组件系统。