REST API 的痛点
传统 REST API 开发中,前后端之间存在一道类型鸿沟:
// 后端定义(TypeScript)
interface User {
id: string;
name: string;
email: string;
}
app.get('/users/:id', async (req, res) => {
const user: User = await db.findUser(req.params.id);
res.json(user);
});
// 前端调用(经典困境)
const res = await fetch('/api/users/123');
const user = await res.json(); // 类型:any 😱
// 开发者需要手动维护类型声明、OpenAPI schema、
// 文档同步……后端改了字段名,前端完全不知道
tRPC 的核心思想:如果前后端都用 TypeScript,那么只需导出后端的路由类型定义,前端就能自动获得完整的类型推断——无需 Swagger、无需 GraphQL schema、无需代码生成。这就是"端到端类型安全"。
核心概念
- Router 路由器,tRPC API 的顶层结构,将多个 Procedure 组合在一起,可以嵌套形成树形结构。
-
Procedure
过程(函数),是 tRPC 的基本操作单元。分三种:
query(读取数据,对应 GET)、mutation(修改数据,对应 POST/PUT/DELETE)、subscription(实时订阅)。 - Context 上下文,每个请求创建一次,传递给所有 Procedure。通常包含数据库连接、当前用户信息等。
- Middleware 中间件,在 Procedure 执行前运行,可修改 Context。常用于认证、日志、速率限制。
- AppRouter 完整的路由类型。前端通过导入这个类型,获得所有 API 的类型信息。这是 tRPC 魔法的核心。
安装与配置
bun add @trpc/server zod
bun add -d @trpc/client # 前端用
服务端:创建 tRPC 实例
// src/trpc/init.ts — tRPC 初始化
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
// 1. 定义 Context 类型
type Context = {
db: Database; // Prisma 客户端等
user: User | null; // 当前登录用户
requestId: string;
};
// 2. 初始化 tRPC(每个应用只做一次)
const t = initTRPC.context<Context>().create({
// 错误格式化(生产环境隐藏内部错误)
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
// 3. 导出构建块
export const router = t.router;
export const publicProcedure = t.procedure;
// 4. 需要登录的 Procedure(通过中间件)
const isAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: { ...ctx, user: ctx.user }, // 类型收窄:user 不再为 null
});
});
export const protectedProcedure = t.procedure.use(isAuthenticated);
定义路由和 Procedure
// src/trpc/routers/users.ts
import { router, publicProcedure, protectedProcedure } from '../init';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
export const usersRouter = router({
// Query — 获取用户列表
list: publicProcedure
.input(z.object({
page: z.number().default(1),
limit: z.number().max(100).default(20),
}))
.query(async ({ ctx, input }) => {
const { page, limit } = input;
return ctx.db.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
});
}),
// Query — 根据 ID 获取用户
byId: publicProcedure
.input(z.string().uuid())
.query(async ({ ctx, input: id }) => {
const user = await ctx.db.user.findUnique({ where: { id } });
if (!user) throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' });
return user;
}),
// Mutation — 创建用户
create: protectedProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
}))
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.user.findUnique({
where: { email: input.email }
});
if (existing) {
throw new TRPCError({ code: 'CONFLICT', message: '邮箱已被注册' });
}
return ctx.db.user.create({ data: input });
}),
// Mutation — 删除用户(需要管理员权限)
delete: protectedProcedure
.input(z.string().uuid())
.mutation(async ({ ctx, input: id }) => {
// 检查是否有权限
if (ctx.user.role !== 'admin' && ctx.user.id !== id) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return ctx.db.user.delete({ where: { id } });
}),
});
合并路由(AppRouter)
// src/trpc/router.ts — 根路由
import { router } from './init';
import { usersRouter } from './routers/users';
import { postsRouter } from './routers/posts';
import { authRouter } from './routers/auth';
export const appRouter = router({
users: usersRouter,
posts: postsRouter,
auth: authRouter,
});
// 导出类型(前端使用这个!)
export type AppRouter = typeof appRouter;
与 Hono 集成(服务端)
// src/index.ts — Hono + tRPC 服务端
import { Hono } from 'hono';
import { trpcServer } from '@hono/trpc-server';
import { appRouter } from './trpc/router';
import { createContext } from './trpc/context';
const app = new Hono();
// 挂载 tRPC 到 /trpc 路径
app.use(
'/trpc/*',
trpcServer({
router: appRouter,
createContext,
})
);
// 其他 REST 路由可以共存
app.get('/health', (c) => c.json({ ok: true }));
export default app;
// src/trpc/context.ts — 创建请求上下文
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { prisma } from '../lib/prisma';
import { verifyToken } from '../lib/auth';
export async function createContext({ req }: FetchCreateContextFnOptions) {
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return { db: prisma, user, requestId: crypto.randomUUID() };
}
前端客户端类型推断
这是 tRPC 最神奇的部分——前端完全不需要写类型定义:
// src/lib/trpc.ts(前端代码)
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../../server/src/trpc/router';
// ↑ 只导入类型,不包含任何服务端运行时代码
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
headers() {
return {
Authorization: `Bearer ${localStorage.getItem('token')}`,
};
},
}),
],
});
// 使用(完整类型推断)
const users = await trpc.users.list.query({ page: 1, limit: 10 });
// users 的类型自动推断为 User[]
const user = await trpc.users.byId.query('some-uuid');
// user 的类型自动推断为 User
const newUser = await trpc.users.create.mutate({
name: '张三',
email: 'zhang@example.com',
});
// 如果传入 name: 123,TypeScript 立即报错!
与 React Query 集成(React 前端)
bun add @trpc/react-query @tanstack/react-query
// src/lib/trpc.ts(React 版)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/src/trpc/router';
export const trpc = createTRPCReact<AppRouter>();
// 在 React 组件中使用
function UserList() {
const { data: users, isLoading } = trpc.users.list.useQuery({ page: 1 });
const createUser = trpc.users.create.useMutation({
onSuccess: () => { /* 刷新列表 */ }
});
if (isLoading) return <div>加载中...</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
);
}
何时用 tRPC,何时用 REST
| 场景 | 推荐 | 理由 |
|---|---|---|
| 全栈 TypeScript(Next.js/Remix) | tRPC | 极致类型安全,开发体验最佳 |
| 需要对外开放 API | REST + OpenAPI | 行业标准,支持非 TS 客户端 |
| 团队同时有 iOS/Android 客户端 | REST | 移动端无法直接用 tRPC |
| 微服务间通信 | gRPC / REST | 跨语言支持 |
| 内部工具/管理后台 | tRPC | 快速迭代,类型保障 |
tRPC 的限制:tRPC 目前不支持文件上传(需要单独的 REST 端点)、缺乏原生 WebSocket 支持(需要 subscription 配合 ws 适配器)、前后端必须共享 TypeScript 代码(monorepo 或 npm 包)。