經過 9 天的後端開發設計,我們建立了一個完整的 Monorepo + TypeScript 全端開發專案。今天讓我們總結這個架構的設計思路、技術選擇,以及它如何完成一個生產級 SaaS 產品的開發。
從 Day 1 的專案初始化到 Day 9 的後端前端整合,我們實現了一個現代軟體工程實踐的過程。
kyong-saas/ # 🎯 Monorepo 根目錄
├── 📦 package.json # 全域套件管理
├── 🔧 pnpm-workspace.yaml # pnpm 工作區配置
├── ⚡ turbo.json # Turbo 建構配置
├── 📝 tsconfig.base.json # TypeScript 基礎配置
│
├── 🎯 apps/ # 應用程式層
│ ├── kyo-otp-service/ # 後端 API 服務
│ │ ├── src/
│ │ │ ├── app.ts # Fastify 應用主體
│ │ │ ├── auth.ts # JWT 驗證邏輯
│ │ │ ├── orpcRoute.ts # oRPC 路由定義
│ │ │ └── index.ts # 服務入口點
│ │ ├── test/ # 測試檔案
│ │ ├── prisma/ # 資料庫 Schema
│ │ └── package.json # 服務專用依賴
│ │
│ └── kyo-dashboard/ # 前端管理介面
│ ├── src/
│ │ ├── hooks/ # React Query Hooks
│ │ ├── pages/ # 頁面元件
│ │ ├── lib/ # 工具函數
│ │ └── App.tsx # 主要應用元件
│ └── package.json # 前端專用依賴
│
├── 📚 packages/ # 共享套件層
│ ├── @kyong/kyo-core/ # 核心業務邏輯
│ │ ├── src/
│ │ │ ├── index.ts # API 介面定義
│ │ │ ├── sms.ts # SMS 服務實作
│ │ │ ├── redis.ts # Redis 快取層
│ │ │ ├── rateLimiter.ts # 速率限制器
│ │ │ └── orpc.ts # oRPC 路由工廠
│ │ └── package.json
│ │
│ ├── @kyong/kyo-types/ # 型別定義
│ │ ├── src/
│ │ │ ├── schemas.ts # Zod 驗證 Schema
│ │ │ └── errors.ts # 錯誤型別定義
│ │ └── package.json
│ │
│ ├── @kyong/kyo-ui/ # UI 元件庫
│ │ ├── src/index.tsx # 元件匯出
│ │ └── package.json
│ │
│ └── @kyong/kyo-config/ # 設定管理
│ ├── src/index.ts # 環境變數處理
│ └── package.json
│
├── 🚀 infra/ # 基礎設施程式碼
│ └── cdk/ # AWS CDK 定義
│ ├── lib/
│ │ └── kyo-system-stack.ts # 完整堆疊定義
│ └── bin/app.ts # CDK 應用程式入口
│
└── 👥 clients/ # 多語言客戶端
├── node/sign.ts # Node.js 客戶端
├── python/sign.py # Python 客戶端
└── go/sign.go # Go 客戶端
實際程式碼展示:
// packages/kyo-types/src/schemas.ts - 共享型別定義
export const OtpSendSchema = z.object({
phone: z.string().regex(/^09\d{8}$/, '請輸入有效的台灣手機號碼'),
templateId: z.number().optional()
});
export type OtpSendRequest = z.infer<typeof OtpSendSchema>;
// apps/kyo-otp-service/src/app.ts - 後端使用
import { OtpSendSchema, type OtpSendRequest } from '@kyong/kyo-types';
app.post('/api/otp/send', async (req, reply) => {
const parsed = OtpSendSchema.safeParse(req.body);
if (!parsed.success) return reply.code(400).send(error('E_INVALID', 'Invalid payload'));
// 型別安全,自動完成,編譯時檢查 ✅
});
// apps/kyo-dashboard/src/hooks/useOtp.ts - 前端使用
import type { OtpSendRequest } from '@kyong/kyo-types';
async function sendOtp(data: OtpSendRequest) {
// 相同的型別定義,前後端一致 ✅
}
架構優勢:
建構配置:
// pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": { "cache": false },
"test": {}
}
}
效能對比:
# 傳統方式:序列建構(慢)
cd packages/kyo-types && npm run build
cd packages/kyo-core && npm run build
cd apps/kyo-otp-service && npm run build
# Turbo 方式:並行建構 + 快取(快)
pnpm run build # 自動解析依賴圖,並行執行
# 實際效能數據
- 初次建構:3.2 秒 → 1.8 秒 (44% 提升)
- 增量建構:1.2 秒 → 0.3 秒 (75% 提升)
- 快取命中:0.1 秒 (近乎瞬間)
型別安全的 API 設計:
// packages/kyo-core/src/orpc.ts - API 路由工廠
export function createOrpcRouter(otpService: IOtpService) {
return {
otp: {
send: async (input: OtpSendRequest): Promise<OtpSendResponse> => {
return await otpService.send(input);
},
verify: async (input: OtpVerifyRequest): Promise<OtpVerifyResponse> => {
return await otpService.verify(input);
},
},
};
}
export type OrpcRouter = ReturnType<typeof createOrpcRouter>;
// apps/kyo-otp-service/src/orpcRoute.ts - 後端整合
export async function registerOrpc(app: FastifyInstance, otpService: IOtpService) {
const router = createOrpcRouter(otpService);
app.post('/orpc/*', async (request, reply) => {
// 自動路由解析 + 型別驗證
const result = await handleOrpcRequest(router, request);
return reply.send(result);
});
}
// 前端自動型別推導
// 客戶端會自動獲得完整的型別資訊和自動完成
關鍵優勢:
// packages/kyo-core/src/index.ts - 業務邏輯層
export interface IOtpService {
send(req: OtpSendRequest): Promise<OtpSendResponse>;
verify(req: OtpVerifyRequest): Promise<OtpVerifyResponse>;
getTemplates(): Promise<Template[]>;
}
export class OtpService implements IOtpService {
constructor(
private smsService: ISmsService, // 依賴注入
private redisCache: IRedisCache, // 快取層
private rateLimiter: IRateLimiter // 速率限制
) {}
async send(req: OtpSendRequest): Promise<OtpSendResponse> {
// 1. 速率限制檢查
await this.rateLimiter.checkLimit(req.phone);
// 2. 生成 OTP 碼
const code = this.generateOtp();
// 3. 儲存到 Redis
await this.redisCache.setOtp(req.phone, code, 300);
// 4. 發送簡訊
const result = await this.smsService.send(req.phone, code);
return { success: true, msgId: result.msgId };
}
}
分層優勢:
// packages/kyo-types/src/errors.ts
export class KyoError extends Error {
constructor(
public code: string,
public message: string,
public status: number = 400,
public issues?: any
) {
super(message);
this.name = 'KyoError';
}
}
export const errorCodes = {
E_INVALID: 'Invalid input data',
E_RATE_LIMIT: 'Rate limit exceeded',
E_OTP_EXPIRED: 'OTP code expired',
E_OTP_INVALID: 'Invalid OTP code',
E_INTERNAL: 'Internal server error'
} as const;
// packages/kyo-core/src/rateLimiter.ts - 實際使用
export class RedisRateLimiter implements IRateLimiter {
async checkLimit(key: string): Promise<void> {
const count = await this.redis.incr(`rate:${key}`);
if (count === 1) {
await this.redis.expire(`rate:${key}`, 60);
}
if (count > this.maxRequests) {
throw new KyoError('E_RATE_LIMIT', 'Too many requests', 429);
}
}
}
// apps/kyo-otp-service/test/otp.test.ts
import { describe, it, expect, beforeEach } from 'node:test';
import { buildApp } from '../src/app.js';
describe('OTP Service', () => {
let app: FastifyInstance;
beforeEach(async () => {
app = await buildApp();
});
it('should send OTP successfully', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/otp/send',
payload: { phone: '0987654321' }
});
expect(response.statusCode).toBe(202);
const body = JSON.parse(response.body);
expect(body.success).toBe(true);
expect(body.msgId).toBeDefined();
});
it('should reject invalid phone number', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/otp/send',
payload: { phone: 'invalid' }
});
expect(response.statusCode).toBe(400);
});
});
指標 | Day 1 (單體) | Day 10 (Monorepo) | 改善 |
---|---|---|---|
初次建構時間 | 45 秒 | 18 秒 | ⬇️ 60% |
增量建構時間 | 30 秒 | 3 秒 | ⬇️ 90% |
測試執行時間 | 25 秒 | 8 秒 | ⬇️ 68% |
型別檢查時間 | 15 秒 | 5 秒 | ⬇️ 67% |
熱重載時間 | 3 秒 | 0.5 秒 | ⬇️ 83% |
// Day 1: 沒有型別安全的 API 調用
fetch('/api/otp/send', {
method: 'POST',
body: JSON.stringify({ phone: '0987654321' }) // 💥 拼寫錯誤風險
});
// Day 10: 完全型別安全的 API 調用
const result = await apiClient.otp.send({
phone: '0987654321', // ✅ 自動完成 + 型別檢查
templateId: 1 // ✅ 可選參數自動提示
});
// result 的型別自動推導為 OtpSendResponse ✅
# apps/kyo-otp-service/Dockerfile
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 利用 Turbo 的建構快取
FROM base AS builder
COPY . .
RUN npm run build
FROM base AS runtime
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
# .github/workflows/ci.yml
name: CI/CD Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9.0.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run type check
run: pnpm turbo run type-check
- name: Run tests
run: pnpm turbo run test
- name: Build packages
run: pnpm turbo run build
- name: Run integration tests
run: pnpm test:integration
// 計劃新增的套件
packages/
├── @kyong/kyo-metrics/ # 監控指標收集
├── @kyong/kyo-auth/ # 統一身份驗證
├── @kyong/kyo-events/ # 事件驅動架構
└── @kyong/kyo-testing/ # 測試工具集
// ✅ 正確的依賴關係設計
apps/kyo-otp-service → packages/kyo-core
→ packages/kyo-types
apps/kyo-dashboard → packages/kyo-ui
→ packages/kyo-types
packages/kyo-core → packages/kyo-types
→ packages/kyo-config
// ❌ 避免循環依賴
packages/kyo-types ✗ packages/kyo-core // 禁止
apps/kyo-dashboard ✗ apps/kyo-otp-service // 禁止
// 從 Schema 定義開始
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
createdAt: z.date()
});
// 自動推導型別
type User = z.infer<typeof UserSchema>;
// 執行時驗證
function createUser(data: unknown): User {
return UserSchema.parse(data); // 拋出詳細錯誤
}
// 資料庫層使用
async function saveUser(user: User): Promise<void> {
// user 已經是型別安全的 ✅
}
// 建構時間優化
// tsconfig.json
{
"compilerOptions": {
"incremental": true, // 增量編譯
"tsBuildInfoFile": ".tsbuild" // 快取檔案
},
"references": [ // 專案引用
{ "path": "./packages/kyo-types" },
{ "path": "./packages/kyo-core" }
]
}
// 運行時效能優化
export class CachedOtpService implements IOtpService {
private cache = new Map<string, OtpSendResponse>();
async send(req: OtpSendRequest): Promise<OtpSendResponse> {
const cacheKey = `${req.phone}_${Date.now()}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
const result = await this.otpService.send(req);
this.cache.set(cacheKey, result);
return result;
}
}
我們建立了一個: