告別 API 文件:使用 tRPC 建構端對端型別安全的應用程式
1. 前後端協作的「契約」難題
在傳統的前後端分離開發中,API 就是兩者之間的「契約」。我們通常透過以下方式維護這份契約:
- RESTful API:依賴 OpenAPI(Swagger)等工具產生並維護詳盡的 API 文件。
- GraphQL:依賴嚴格的 Schema Definition Language(SDL)定義資料結構與操作。
這些方案確實有效,但都存在一項共同問題:契約與實作彼此分離。當前端開發者呼叫 API 時,他相信該 API 會回傳文件或 schema 中描述的資料結構。但如果後端實作發生變更(例如修改欄位名稱),文件或 schema 卻未能及時更新,這種不一致只會在執行階段才暴露,進而造成 Bug。
有沒有一種方法,能讓這份「契約」自動與實作保持同步,甚至在編譯時便發現不一致?
2. tRPC 的核心思想:共享型別,而非 schema
tRPC(TypeScript Remote Procedure Call)提出一項激進但極其簡單的方案:如果前端與後端都使用 TypeScript,為什麼不直接共享型別?
tRPC 讓你能將純 TypeScript 函式撰寫成後端 API,接著直接在前端呼叫,並享有完整的型別推斷與自動完成,就像呼叫本機模組中的函式一樣。
它不依賴任何 schema 或程式碼產生。唯一的「契約」就是 TypeScript 型別本身。
3. 它如何運作?
tRPC 的魔法來自型別推斷,以及一點巧妙的封裝。
a. 後端:定義 API Router
在後端(通常是 Node.js 服務),你使用 tRPC 函式建立一個或多個「Router」。每個 Router 都是一組可供呼叫的「Procedure」,也就是 API 端點。
// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // 使用 Zod 进行运行时校验
const t = initTRPC.create();
export const appRouter = t.router({
// 定义一个名为 `getUser` 的查询 procedure
getUser: t.procedure
.input(z.object({ userId: z.string() }))
.query(({ input }) => {
// 在这里查询数据库或执行其他逻辑
const user = { id: input.userId, name: 'Alex' };
return user;
}),
// 定义一个名为 `createUser` 的变更 procedure
createUser: t.procedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
const user = { id: `${Math.random()}`, name: input.name };
return user;
}),
});
// 导出 router 的类型定义
export type AppRouter = typeof appRouter;b. 前端:建立用戶端並呼叫 API
在前端,你只需要從後端匯入 AppRouter 這個型別。請注意,匯入的只有型別,而不是任何伺服器端程式碼。
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 只导入类型
export const trpc = createTRPCReact<AppRouter>();現在,你可以在 React 元件中像呼叫本機函式一樣呼叫 API,並獲得完整的型別安全與自動完成!
// client/components/UserInfo.tsx
import { trpc } from '../trpc';
function UserInfo({ userId }: { userId: string }) {
// `useQuery` 的第一个参数是 procedure 的路径
// 你输入 `trpc.` 时,IDE 会自动提示 `getUser` 和 `createUser`
const userQuery = trpc.getUser.useQuery({ userId });
if (userQuery.isLoading) {
return <div>Loading...</div>;
}
// `userQuery.data` 的类型被自动推断为 { id: string; name: string }
return <div>User: {userQuery.data?.name}</div>;
}如果此時後端開發者將 getUser 回傳的 name 欄位重新命名為 fullName,前端的 userQuery.data?.name 會立即在 TypeScript 編譯時報錯,而不是等到執行階段才被發現。
4. 為什麼選擇 tRPC?
- 絕對的端對端型別安全:這是它的核心價值,能消除因 API 契約不一致而產生的一整類 Bug。
- 卓越的開發體驗:IDE 自動完成功能讓你不必查閱文件或猜測 API 結構。重構也變得異常輕鬆且安全。
- 不需產生程式碼:沒有額外的建置步驟,回饋循環非常快速。
- 輕量且靈活:tRPC 本身非常小,並可與任何前端框架及後端服務整合。
結論
tRPC 為全端 TypeScript 開發帶來前所未有的流暢體驗。透過消除對 API 文件與 schema 的依賴,它讓前端和後端之間的協作變得無縫且極其安全。在 Monorepo 架構下,這些優勢會進一步放大。如果你正在建構全端 TypeScript 應用程式,tRPC 絕對是一項值得投入時間嘗試的革命性工具。