跳转至

tRPC 端到端类型安全 API

一句话概述:tRPC 让你在 TypeScript 全栈项目中实现端到端类型安全——改了后端函数签名,前端调用处自动报错提示,不需要写 Schema、不需要代码生成、不需要 REST/GraphQL。

核心知识点

概念白话解释
端到端类型安全后端改了返回类型,前端 TypeScript 编译时就能发现错误
ProceduretRPC 的"接口",类似 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.tsexport 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 文档