前後端類型共享策略
前後端分離時,如何確保 API 類型一致?本篇將介紹從簡單到進階的類型共享策略。
一、 為什麼要共享類型?
1.1 問題場景
typescript
// 後端定義
interface User {
id: number;
name: string;
email: string;
}
// 前端定義(可能不一致!)
interface User {
id: number;
name: string;
mail: string; // 欄位名稱錯誤
}1.2 共享類型的好處
- 減少錯誤:前後端使用相同的類型定義
- 提升效率:不需要重複定義
- 自動同步:類型變更自動反映到兩端
- 更好的 IDE 支援:自動完成、錯誤檢查
二、 策略一:共享類型檔案
2.1 專案結構
project/
├── packages/
│ ├── shared/ # 共享類型
│ │ ├── package.json
│ │ └── src/
│ │ └── types.ts
│ ├── backend/ # 後端專案
│ │ └── package.json
│ └── frontend/ # 前端專案
│ └── package.json
└── package.json # 根 package.json2.2 設定 Monorepo
package.json(根目錄):
json
{
'private': true,
'workspaces': ['packages/*']
}packages/shared/package.json:
json
{
'name': '@myapp/shared',
'version': '1.0.0',
'main': 'src/index.ts',
'types': 'src/index.ts'
}2.3 定義共享類型
typescript
// packages/shared/src/types.ts
export interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
export interface CreateUserInput {
name: string;
email: string;
password: string;
}
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export type UserResponse = ApiResponse<User>;
export type UserListResponse = ApiResponse<User[]>;2.4 使用
typescript
// packages/backend/src/controllers/user.ts
import { User, CreateUserInput, UserResponse } from '@myapp/shared';
export async function createUser(input: CreateUserInput): Promise<User> {
// ...
}
// packages/frontend/src/api/user.ts
import { User, CreateUserInput, UserResponse } from '@myapp/shared';
export async function createUser(input: CreateUserInput): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(input),
});
const result: UserResponse = await response.json();
return result.data;
}三、 策略二:使用 Zod 共享
3.1 定義 Schema
typescript
// packages/shared/src/schemas/user.ts
import { z } from 'zod';
export const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
});
export const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
password: z.string().min(8),
});
// 匯出類型
export type User = z.infer<typeof userSchema>;
export type CreateUserInput = z.infer<typeof createUserSchema>;3.2 後端驗證
typescript
// packages/backend/src/routes/user.ts
import { createUserSchema, userSchema } from '@myapp/shared';
app.post('/users', (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = await createUser(result.data);
// 確保回應符合 schema
res.json(userSchema.parse(user));
});3.3 前端驗證
typescript
// packages/frontend/src/api/user.ts
import {
createUserSchema,
userSchema,
User,
CreateUserInput,
} from '@myapp/shared';
export async function createUser(input: CreateUserInput): Promise<User> {
// 發送前驗證
const validated = createUserSchema.parse(input);
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(validated),
});
const data = await response.json();
// 回應驗證
return userSchema.parse(data);
}四、 策略三:tRPC
4.1 什麼是 tRPC?
tRPC 是一個端到端類型安全的 RPC(Remote Procedure Call)框架,讓前端直接呼叫後端函式,自動獲得類型。
4.2 後端設定
typescript
// packages/backend/src/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;typescript
// packages/backend/src/routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
getAll: publicProcedure.query(async () => {
return await db.user.findMany();
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await db.user.findUnique({ where: { id: input.id } });
}),
create: publicProcedure
.input(
z.object({
name: z.string(),
email: z.string().email(),
})
)
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
});typescript
// packages/backend/src/server.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { userRouter } from './routers/user';
const appRouter = router({
user: userRouter,
});
export type AppRouter = typeof appRouter;
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);4.3 前端使用
typescript
// packages/frontend/src/trpc.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@myapp/backend';
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});typescript
// packages/frontend/src/pages/Users.vue
<script setup lang='ts'>
import { trpc } from '../trpc'
// 完全類型安全!
const users = await trpc.user.getAll.query()
// users 自動推論為 User[]
const user = await trpc.user.getById.query({ id: 1 })
// user 自動推論為 User | null
const newUser = await trpc.user.create.mutate({
name: 'John',
email: 'john@example.com'
})
// newUser 自動推論為 User
</script>五、 策略四:OpenAPI/Swagger 生成
5.1 從 OpenAPI 生成類型
bash
npm install -D openapi-typescriptbash
npx openapi-typescript http://localhost:3000/api-docs/swagger.json -o src/types/api.ts5.2 生成的類型
typescript
// 自動生成
export interface paths {
'/users': {
get: {
responses: {
200: {
content: {
'application/json': components['schemas']['UserList'];
};
};
};
};
post: {
requestBody: {
content: {
'application/json': components['schemas']['CreateUser'];
};
};
};
};
}
export interface components {
schemas: {
User: {
id: number;
name: string;
email: string;
};
};
}5.3 使用生成的類型
typescript
import { paths, components } from './types/api';
type User = components['schemas']['User'];
type CreateUserInput =
paths['/users']['post']['requestBody']['content']['application/json'];六、 策略比較
| 策略 | 複雜度 | 即時類型 | 執行驗證 | 適用場景 |
|---|---|---|---|---|
| 共享類型檔案 | 低 | 是 | 否 | 小型專案 |
| Zod 共享 | 中 | 是 | 是 | 中型專案 |
| tRPC | 中 | 是 | 是 | 全棧專案 |
| OpenAPI 生成 | 高 | 需重新生成 | 視後端 | 多團隊 |
七、 最佳實踐
7.1 類型組織
typescript
// packages/shared/src/index.ts
// 實體類型
export * from './entities/user';
export * from './entities/post';
// API 類型
export * from './api/requests';
export * from './api/responses';
// Schema(如果使用 Zod)
export * from './schemas/user';
export * from './schemas/post';7.2 版本管理
typescript
// 使用版本化的 API 類型
export namespace V1 {
export interface User {
id: number;
name: string;
}
}
export namespace V2 {
export interface User {
id: number;
firstName: string;
lastName: string;
}
}總結
| 策略 | 優點 | 缺點 |
|---|---|---|
| 共享類型 | 簡單直接 | 無執行驗證 |
| Zod | 編譯+執行驗證 | 需額外依賴 |
| tRPC | 完整類型安全 | 需要改架構 |
| OpenAPI | 語言無關 | 需維護文件 |
> **選擇建議**:
- 小型專案:共享類型檔案
- 需要驗證:Zod 共享
- 全棧 TypeScript:tRPC
- 多語言/多團隊:OpenAPI
進階挑戰
- 設定一個 pnpm workspace monorepo
- 使用 tRPC 建立一個完整的 CRUD API
- 從 OpenAPI 規格生成前端 API 客戶端