一句话总结:tRPC 用 TypeScript 类型消除了 API 契约的重复定义——一个 Router 文件自动生成服务端路由与客户端类型,是全栈 TypeScript 项目最高效的 API 方案。
1. tRPC 核心哲学
tRPC 的设计目标是通过 类型推导 和 零配置 消除传统 API 开发的样板代码。
1.1 核心优势
| 特性 | 传统 API | tRPC |
|---|---|---|
| Schema 定义 | OpenAPI / GraphQL SDL 手写 | TypeScript 类型自动推导 |
| 类型同步 | 手动维护 DTO / codegen | 变更即同步,零延迟 |
| 代码生成 | Swagger / GraphQL Codegen | 无需生成,运行时代码复用 |
| 包体积 | SDK 依赖 + 生成代码 | 零运行时 Schema 开销 |
| 学习曲线 | 学习 OpenAPI / GraphQL 语法 | TypeScript 即全部 |
| IDE 支持 | 需插件扩展 | 原生 TypeScript 体验 |
1.2 对比 GraphQL / REST / gRPC
| 维度 | tRPC | GraphQL | REST | gRPC |
|---|---|---|---|---|
| 类型安全 | ✅ 编译时 | ✅ Schema 层面 | ❌ 文档层面 | ✅ Protobuf |
| 浏览器支持 | ✅ HTTP | ✅ HTTP | ✅ HTTP | ⚠️ Web Proxy |
| 多语言 | ❌ TS only | ✅ Apollo 全语言 | ✅ 通用 | ✅ 多语言生成 |
| 实时 | ✅ Subscription | ✅ Subscription | SSE / WS | 原生双向流 |
| 工具生态 | 快速增长 | 成熟丰富 | 成熟 | 云原生成熟 |
选型法则:全栈 TypeScript + 内部 API → tRPC;多端聚合 + 外部 API → GraphQL;高吞吐微服务间 → gRPC;简单开放 API → REST。
2. 核心概念
2.1 Procedure:API 的基本单元
Procedure 是 tRPC 的"函数",封装了输入校验、授权和逻辑执行。
// router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
// query procedure: 只读操作
const userQuery = t.procedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
return ctx.db.user.findUnique({ where: { id: input.id } });
});
// mutation procedure: 写操作
const createUser = t.procedure
.input(
z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
)
.mutation(async ({ input, ctx }) => {
return ctx.db.user.create({ data: input });
});
2.2 Router:Procedure 的命名空间
const appRouter = t.router({
user: t.router({
byId: userQuery,
create: createUser,
list: t.procedure
.input(
z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
})
)
.query(async ({ input, ctx }) => {
const users = await ctx.db.user.findMany({
take: input.limit,
cursor: input.cursor ? { id: input.cursor } : undefined,
});
return {
users,
nextCursor: users[users.length - 1]?.id,
};
}),
}),
post: t.router({
// ...
}),
});
export type AppRouter = typeof appRouter;
2.3 Middleware:横切关注点
// 认证中间件
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
...ctx,
user: ctx.user, // 类型收窄
},
});
});
// 带权限的 procedure
const authedProcedure = t.procedure.use(isAuthed);
// 日志中间件
const logger = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
console.log(`[${type}] ${path} — ${Date.now() - start}ms`);
return result;
});
const loggedProcedure = t.procedure.use(logger);
2.4 Context:请求上下文
// context.ts
import { CreateNextContextOptions } from "@trpc/server/adapters/next";
export async function createContext({ req }: CreateNextContextOptions) {
const token = req.headers.authorization?.split(" ")[1];
const user = token ? await verifyToken(token) : null;
return {
user,
db: prisma,
req,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;
2.5 Transformer:序列化增强
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
const t = initTRPC.create({
transformer: superjson, // 支持 Date, Map, Set, BigInt
});
tRPC 默认使用 JSON.stringify,无法序列化 Date 等类型。superjson 插件完美解决这个问题。
3. 完整 CRUD 实战
3.1 服务端
// server/routers/post.ts
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { z } from "zod";
export const postRouter = router({
list: publicProcedure
.input(
z.object({
search: z.string().optional(),
tag: z.string().optional(),
limit: z.number().min(1).max(50).default(10),
page: z.number().min(1).default(1),
})
)
.query(async ({ input, ctx }) => {
const where = {
...(input.search && {
title: { contains: input.search },
}),
...(input.tag && {
tags: { has: input.tag },
}),
};
const [posts, total] = await Promise.all([
ctx.db.post.findMany({
where,
take: input.limit,
skip: (input.page - 1) * input.limit,
orderBy: { createdAt: "desc" },
include: { author: true },
}),
ctx.db.post.count({ where }),
]);
return {
posts,
pagination: {
page: input.page,
limit: input.limit,
total,
totalPages: Math.ceil(total / input.limit),
},
};
}),
byId: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
const post = await ctx.db.post.findUnique({
where: { id: input.id },
include: { author: true, comments: true },
});
if (!post) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Post not found",
});
}
return post;
}),
create: protectedProcedure
.input(
z.object({
title: z.string().min(1).max(200),
content: z.string().min(1).max(50000),
tags: z.array(z.string().max(50)).max(10),
published: z.boolean().default(false),
})
)
.mutation(async ({ input, ctx }) => {
return ctx.db.post.create({
data: {
...input,
authorId: ctx.user.id,
},
});
}),
update: protectedProcedure
.input(
z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).max(50000).optional(),
published: z.boolean().optional(),
})
)
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
return ctx.db.post.update({
where: { id, authorId: ctx.user.id },
data,
});
}),
});
3.2 客户端 React Hooks
// hooks/usePosts.ts
import { trpc } from "@/utils/trpc";
export function usePosts(search?: string) {
return trpc.post.list.useQuery({
search,
limit: 10,
page: 1,
});
}
// components/PostList.tsx
export function PostList() {
const { data, isLoading, fetchNextPage } = trpc.post.list.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
}
);
if (isLoading) return <Skeleton />;
return (
<div>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
<button onClick={() => fetchNextPage()}>加载更多</button>
</div>
);
}
// mutation
export function CreatePostForm() {
const utils = trpc.useContext();
const mutation = trpc.post.create.useMutation({
onSuccess: () => {
utils.post.list.invalidate(); // 自动刷新列表
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
mutation.mutate({
title: "新文章",
content: "内容...",
tags: ["trpc"],
});
}}
>
{/* ... */}
</form>
);
}
4. React Query 集成
tRPC 底层使用 TanStack Query(原 React Query),所有 Query 的缓存、重试、预取、乐观更新能力完整保留。
4.1 核心 Hooks
| Hook | 用途 | 对应 Query |
|---|---|---|
useQuery | 读取数据 | Query |
useInfiniteQuery | 无限滚动/分页 | Query |
useMutation | 修改数据 | Mutation |
useSubscription | 实时推送 | Subscription |
useContext | 获取 QueryClient | — |
4.2 乐观更新
const mutation = trpc.post.like.useMutation({
onMutate: async (postId) => {
await utils.post.byId.cancel({ id: postId });
const previousPost = utils.post.byId.getData({ id: postId });
utils.post.byId.setData({ id: postId }, (old) =>
old ? { ...old, likeCount: old.likeCount + 1 } : old
);
return { previousPost };
},
onError: (err, postId, context) => {
utils.post.byId.setData({ id: postId }, context?.previousPost);
},
onSettled: (postId) => {
utils.post.byId.invalidate({ id: postId });
},
});
4.3 预取
// 鼠标悬停时预取
function PostLink({ id }: { id: string }) {
const utils = trpc.useContext();
return (
<Link
href={`/posts/${id}`}
onMouseEnter={() => utils.post.byId.prefetch({ id })}
>
查看文章
</Link>
);
}
5. Next.js 集成
5.1 Pages Router
// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";
export default createNextApiHandler({
router: appRouter,
createContext,
onError: ({ error, path }) => {
console.error(`[tRPC Error] ${path}: ${error.message}`);
},
});
5.2 App Router (RSC)
// app/posts/page.tsx (Server Component)
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";
export default async function PostsPage() {
const caller = appRouter.createCaller(await createContext());
const posts = await caller.post.list({ limit: 10 });
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
5.3 Client Component 混合
// app/posts/PostList.tsx
"use client";
import { trpc } from "@/utils/trpc";
export function PostList() {
const { data, isLoading } = trpc.post.list.useQuery({ limit: 10 });
// ...
}
6. 订阅(Subscription)
// server/routers/notification.ts
import { observable } from "@trpc/server/observable";
export const notificationRouter = router({
onNewNotification: protectedProcedure
.subscription(({ ctx }) => {
return observable<string>((emit) => {
const handler = (data: string) => emit.next(data);
// 订阅 Redis PubSub
redisSub.subscribe(`user:${ctx.user.id}:notifications`);
redisSub.on("message", handler);
return () => {
redisSub.unsubscribe(`user:${ctx.user.id}:notifications`);
redisSub.off("message", handler);
};
});
}),
});
客户端:
function NotificationBadge() {
const { data } = trpc.notification.onNewNotification.useSubscription(
undefined,
{
onData: (data) => {
toast(data);
},
}
);
return <Badge count={unreadCount} />;
}
7. 与 GraphQL / REST 的混合策略
7.1 三层 API 架构
┌─────────────────────────────────────┐
│ 多端客户端(Web / iOS / Android) │
└──────────────┬──────────────────────┘
│ GraphQL (Apollo Client)
▼
┌─────────────────────────────────────┐
│ GraphQL Gateway (Apollo Router) │
│ 聚合层 / BFF / 权限控制 │
└──────────────┬──────────────────────┘
│ tRPC / gRPC
▼
┌─────────────────────────────────────┐
│ TypeScript 微服务(tRPC) │
│ 内部 API,端到端类型安全 │
└─────────────────────────────────────┘
7.2 何时用 tRPC,何时用 GraphQL
| 场景 | 推荐 |
|---|---|
| 全栈 TypeScript 内部 API | tRPC |
| 移动端 / 第三方接入 | GraphQL |
| 已有 GraphQL 生态(Federation) | GraphQL |
| 需要 Swagger / OpenAPI 文档 | REST / GraphQL |
| AI / LLM 工具调用 | GraphQL(标准化 schema) |
8. 生产最佳实践
8.1 错误处理
// 统一错误格式化
import { TRPCError } from "@trpc/server";
import { initTRPC } from "@trpc/server";
const t = initTRPC.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
8.2 Auth 集成
// NextAuth / Clerk 集成
export const protectedProcedure = t.procedure
.use(async ({ ctx, next }) => {
const user = await getAuth(ctx.req);
if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, user } });
});
8.3 部署
| 平台 | 适配器 |
|---|---|
| Vercel Edge | fetch adapter |
| Vercel Node | next adapter |
| AWS Lambda | aws-lambda adapter |
| Express | express middleware |
| Fastify | fastify plugin |
9. 一句话总结
- 核心理念:TypeScript 类型即 API 契约,零重复、零代码生成
- Router + Procedure:命名空间组织 API,input/output 天然类型安全
- Middleware:认证、日志、缓存复用,类似 Express 中间件
- React Query:底层缓存、重试、乐观更新、预取能力完整继承
- Next.js:Pages Router / App Router / RSC 全适配
- 混合策略:tRPC(内部 TS 全栈)+ GraphQL(多端聚合)互补
FAQ
Q1:tRPC 是否绑定 Next.js?
A:不绑定。tRPC 支持多种框架:Next.js、React(Vite / CRA)、Svelte、Vue、React Native、Express、Fastify 等。Next.js 集成最完善,但非必需。
Q2:非 TypeScript 客户端(如 Swift / Kotlin)怎么调用 tRPC?
A:tRPC 仅天生支持 TypeScript。如需多语言接入,可在 tRPC 服务端增加 OpenAPI 生成(zod-to-openapi),或语言桥接层(如 Kotlin 通过 HTTP POST 调用并手动维护类型)。这种情况下 GraphQL 或 REST 更合适。
Q3:tRPC 的性能与 REST/GraphQL 相比如何?
A:tRPC 使用 HTTP JSON,序列化开销与 REST 相当。无 Schema 解析和查询规划开销,所以多数场景比 GraphQL 更快。与 gRPC(二进制 Protobuf)相比,吞吐量和延迟稍差,但开发效率更高。
Q4:如何实现 tRPC 的限流?
A:在 middleware 中集成限流库(如 @upstash/ratelimit):
const rateLimit = t.middleware(async ({ path, ctx, next }) => {
const result = await ratelimit.limit(ctx.user?.id ?? ctx.req.ip);
if (!result.success) {
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
}
return next();
});
Q5:tRPC 的订阅在服务端如何扩展?
A:tRPC Subscription 基于 WebSocket,多节点部署时需要共享事件源。建议:① Redis PubSub 做跨节点广播;② 使用 SSE 方案替代 WebSocket(部分 adapter 支持);③ 结合 Ably / Pusher 等托管实时服务。
相关阅读
- GraphQL vs REST vs gRPC vs tRPC:选型指南 — 四范式深度对比
- 前端工程化 — TypeScript 与 React 工程实践
- API 缓存与性能优化 — 性能基准与缓存策略
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。