🚀 1. 开发体验
对比项 | tRPC | Next.js API Routes |
---|---|---|
代码量 | 少,前后端共用逻辑和类型 | 需要编写前后端的类型和接口 |
前端调用方式 | trpc.add.useQuery() 直接调用 |
fetch('/api/add') 手动调用 |
输入参数校验 | 使用 zod 直接在路由中校验 |
需手动编写 if...else 逻辑 |
类型提示 | 前后端类型一致,自动推导 | 需要手动定义和维护前后端类型 |
错误处理 | 内置 errorLink 和自动捕获 |
需手动在try-catch 中处理错误 |
解释:
- tRPC 中,前端的输入和后端的接口是完全类型对齐的,前端可以自动感知后端的参数和返回值类型。
- Next.js API 需要手动维护前端调用和后端接口的类型,容易出错。
📦 2. 类型安全 (Type-Safe)
对比项 | tRPC | Next.js API Routes |
---|---|---|
类型自动生成 | ✅ 全程类型安全,前后端类型共享 | ❌ 需要手动编写前后端类型声明 |
类型检测 | ✅ 传错类型,开发时就会报错 | ❌ 类型错误会在运行时才发现 |
TypeScript 友好 | ✅ 类型从输入到输出自动联动 | ❌ 前端和后端类型不连贯 |
解释:
- tRPC 可以确保端到端的类型安全 ,前端调用的
trpc.add.useQuery({ a: 1, b: 2 })
会自动检查类型是否正确。 - Next.js API Routes 只能在接口中用
zod
、Yup
等库手动验证参数,但前端调用fetch('/api/add')
不会有类型提示。
⚡️ 3. 性能和性能优化
对比项 | tRPC | Next.js API Routes |
---|---|---|
性能 | 比 Next.js API 更快 (无 JSON 解析) | 依赖 fetch() 解析和序列化 JSON |
请求开销 | 直接在前后端共享对象和数据流 | 使用 HTTP 请求,数据需要序列化 |
数据传输 | 不需要 JSON 序列化 (内部优化) | 需手动 JSON 序列化/反序列化 |
服务端渲染 (SSR) | 支持 SSG 和 SSR | 需要手动调用 getServerSideProps |
缓存和预取 | 内置 React Query 支持的缓存 | 需要自己实现 useSWR |
解释:
- tRPC 使用React Query 做数据请求,支持缓存、重试、请求状态等功能。
- Next.js API 必须依赖
fetch
,每次调用都得序列化/反序列化 JSON 数据,多了一步开销。
总结:
- 如果你想要一个高性能的调用,tRPC 是更好的选择,因为它不需要请求序列化 JSON 数据。
- Next.js API Routes 的请求更接近 REST API,性能略逊于 tRPC。
🛠️ 4. 维护成本
对比项 | tRPC | Next.js API Routes |
---|---|---|
接口维护成本 | 低:类型定义和逻辑统一管理 | 高:前后端类型分离,容易错漏 |
路由定义 | 自动生成 /api/trpc 路由 |
每个 API 路由都需要手动定义 |
代码复用性 | 复用率高,前后端共用类型 | 前后端类型分离,重用性低 |
中间件 | 支持中间件,可统一管理请求 | 需手动实现 middleware |
学习成本 | 中:需要学习 tRPC 的语法 | 低:熟悉 Next.js 的 fetch 即可 |
解释:
- tRPC 的类型共享,意味着你不需要在前端和后端分别维护 API 类型。
- Next.js API 中,前后端的类型独立,每次修改 API 需要同步更新前端的调用逻辑,成本更高。
🔥 5. 使用场景
场景 | tRPC | Next.js API Routes |
---|---|---|
简单的增删改查 | ✅ 适用,简单高效,省去序列化 | ✅ 适用,传统的 API 方式 |
端到端类型安全 | ✅ 更好,类型全链路可用 | ❌ 需手动定义类型 |
前后端协作 | ✅ 最佳,前后端共享类型 | ❌ 需手动同步类型 |
需要中间件支持 | ✅ 支持,易于编写中间件 | ❌ 需手动实现中间件 |
数据同步和缓存 | ✅ 内置缓存 (React Query) | ❌ 需自己实现 SWR/React Query |
第三方 API 调用 | ❌ 不擅长与外部 API 通信 | ✅ 适用于 REST 和外部 API |
总结:
- tRPC 适合 :前后端共享类型的 SPA 项目,如:面试平台、管理面板、内部工具等。
- Next.js API Routes 适合 :传统的 API 开发 ,需要与外部 API 通信,如:支付网关、外部 API 代理等。
🔍 6. 详细对比总结
对比项 | tRPC | Next.js API Routes |
---|---|---|
类型安全 | ✅ 自动前后端类型一致 | ❌ 需手动同步 |
API 语法 | 🚀 直接用 trpc.add.useQuery |
需要 fetch('/api/add') |
中间件支持 | ✅ 轻松支持中间件 | ❌ 需手动实现 |
数据序列化 | 🚀 tRPC 内部优化,序列化开销低 | ❌ 必须手动 JSON 序列化 |
缓存 | ✅ 通过 React Query 自动缓存 | ❌ 需手动实现 |
调用方式 | 🚀 直接使用 React Hook | ❌ 需 fetch |
学习成本 | ⚠️ 中,需要理解 tRPC 概念 | ✅ 低,熟悉 REST 即可 |
SSR 支持 | ✅ 支持 SSR 和 SSG | ✅ 支持 SSR 和 SSG |
推荐场景 | 🚀 适合前后端类型一致的 SPA | 🚀 适合与外部 API 通信的场景 |
💡 总结
- 如果前后端共用的类型一致性 对你来说非常重要,tRPC 是最好的选择,类型自动联动。
- 如果你需要与外部 REST API 交互 ,或需要与第三方 API 通信,那使用 Next.js 的 API Routes 会更自然。
- tRPC 适合SPA 应用,面板管理系统,BFF(后端为前端)架构。
- Next.js API Routes 适合与外部系统的通信,比如 OAuth 登录、Webhook 等。
🚀 你应该怎么选择?
- 如果你想要前后端共享类型、端到端的类型安全,选 tRPC。
- 如果你更熟悉 REST API,并且需要与外部系统交互,选 Next.js API Routes。
创建一个基于tRPC的 Todo 应用
📦 1. 准备工作
确保你的项目已经完成了 tRPC 的基础配置(参考之前的教程),并安装了以下依赖:
perl
npm install @trpc/client @trpc/server @trpc/react-query @trpc/next zod react-query
📂 2. 项目结构
我们将在项目中新增文件来实现一个简单的 Todo 应用,更新后的结构如下:
arduino
my-trpc-app/
├── app/
│ ├── page.tsx // 主页,展示 Todo 列表
│ └── api/
│ └── trpc/
│ └── route.ts // tRPC API 路由
├── server/
│ ├── trpc.ts // tRPC 服务器配置
│ └── router/
│ ├── appRouter.ts // 主路由
│ └── todoRouter.ts // Todo 路由
├── utils/
│ └── trpc.ts // tRPC 客户端配置
└── tsconfig.json // TypeScript 配置
🔧 3. 配置 Todo 路由
文件 :
server/router/todoRouter.ts
typescript
import { router, procedure } from '../trpc';
import { z } from 'zod';
let todos: { id: number; title: string; completed: boolean }[] = [];
export const todoRouter = router({
// 获取所有 Todo
getAll: procedure.query(() => {
return todos;
}),
// 添加 Todo
add: procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => {
const newTodo = {
id: todos.length + 1,
title: input.title,
completed: false,
};
todos.push(newTodo);
return newTodo;
}),
// 切换完成状态
toggle: procedure
.input(z.object({ id: z.number() }))
.mutation(({ input }) => {
const todo = todos.find((t) => t.id === input.id);
if (!todo) throw new Error('Todo not found');
todo.completed = !todo.completed;
return todo;
}),
// 删除 Todo
delete: procedure
.input(z.object({ id: z.number() }))
.mutation(({ input }) => {
todos = todos.filter((t) => t.id !== input.id);
return { success: true };
}),
});
🔗 4. 将 Todo 路由集成到主路由
文件 :
server/router/appRouter.ts
typescript
import { router } from '../trpc';
import { todoRouter } from './todoRouter';
export const appRouter = router({
todo: todoRouter, // 注册 Todo 路由
});
// 导出 tRPC 路由的类型
export type AppRouter = typeof appRouter;
🖥️ 5. 在页面中调用 tRPC
文件 :
app/page.tsx
javascript
'use client';
import { trpc } from '@/utils/trpc';
import { useState } from 'react';
export default function Home() {
const [newTodo, setNewTodo] = useState('');
const utils = trpc.useContext();
const { data: todos, isLoading } = trpc.todo.getAll.useQuery();
const addTodo = trpc.todo.add.useMutation({
onSuccess: () => {
utils.todo.getAll.invalidate(); // 刷新列表
},
});
const toggleTodo = trpc.todo.toggle.useMutation({
onSuccess: () => {
utils.todo.getAll.invalidate(); // 刷新列表
},
});
const deleteTodo = trpc.todo.delete.useMutation({
onSuccess: () => {
utils.todo.getAll.invalidate(); // 刷新列表
},
});
if (isLoading) return <p>加载中...</p>;
return (
<div style={{ padding: '20px' }}>
<h1>Todo 应用</h1>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
placeholder="新增 Todo"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button
onClick={() => {
addTodo.mutate({ title: newTodo });
setNewTodo('');
}}
>
添加
</button>
</div>
<ul>
{todos?.map((todo) => (
<li key={todo.id} style={{ marginBottom: '10px' }}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer',
}}
onClick={() => toggleTodo.mutate({ id: todo.id })}
>
{todo.title}
</span>
<button onClick={() => deleteTodo.mutate({ id: todo.id })}>
删除
</button>
</li>
))}
</ul>
</div>
);
}
💡 6. 测试应用
-
启动开发服务器:
arduinonpm run dev
-
打开浏览器访问
http://localhost:3000/
,你会看到一个简单的 Todo 应用,可以:- 添加新的 Todo 项目。
- 切换 Todo 的完成状态。
- 删除 Todo。
🎉 7. 总结
通过这个 Todo 应用,你已经学会了以下内容:
- tRPC 查询与变更 :如何使用
query
和mutation
。 - 端到端类型安全:通过 TypeScript,在前后端共享类型定义。
- 状态管理 :通过
useContext
实现自动刷新列表。
下一步可以尝试优化 UX,例如添加加载状态和错误提示,或将数据持久化到数据库。