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 superjson

2.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 appRouter

4. 客户端调用

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

维度tRPCServer ActionsRoute Handlers
类型安全✅ 端到端自动✅ 端到端自动⚠️ 手动定义
客户端缓存✅ React Query
批量请求httpBatchLink
中间件✅ 管道式⚠️ 手动⚠️ 手动
表单集成&lt;form action&gt;
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

至此,第三篇「渲染模式与数据获取」全部完成。下一篇将深入组件系统。