告別 API 文件:使用 tRPC 建構端對端型別安全的應用程式

5 分鐘

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 絕對是一項值得投入時間嘗試的革命性工具。