昨天我們設計了後端整體架構,今天要開始實作第一個微服務:Kyo-OTP 驗證服務。這個是新的客戶需求,也適合打造成微服務成為工作室SaaS的第一個業務服務。
OTP 服務可以解決這些重複性問題,成為所有專案的共用基礎設施。
核心業務邏輯
技術架構要求
1. 初始化 Fastify + TypeScript 專案
cd apps
# 建立後端服務目錄
mkdir kyo-otp-service && cd kyo-otp-service
# 初始化 package.json
pnpm init
2. 設定 package.json 配置
{
"name": "@kyong/kyo-otp-service",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/app.ts",
"build": "tsc",
"start": "node dist/app.js",
"lint": "eslint src --ext .ts",
"test": "vitest"
},
"dependencies": {
"fastify": "^4.28.1",
"@fastify/cors": "^9.0.1",
"@fastify/helmet": "^11.1.1",
"redis": "^4.6.14",
"prisma": "^5.16.1",
"@prisma/client": "^5.16.1"
},
"devDependencies": {
"@types/node": "^20.14.11",
"typescript": "^5.5.3",
"tsx": "^4.16.2",
"eslint": "^8.57.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"vitest": "^2.0.2"
}
}
3. 安裝必要的後端套件
# 核心框架
pnpm add fastify @fastify/cors @fastify/helmet @fastify/rate-limit
# 資料庫相關
pnpm add redis @prisma/client prisma
# 驗證與型別
pnpm add zod @kyong/kyo-types @kyong/kyo-core
# 開發工具
pnpm add -D tsx @types/node typescript eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser vitest
# 環境變數
pnpm add dotenv
4. 建立 TypeScript 配置
// tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowImportingTsExtensions": false,
"strict": true,
"noEmit": false,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/routes/*": ["./src/routes/*"],
"@/services/*": ["./src/services/*"],
"@/middleware/*": ["./src/middleware/*"],
"@/utils/*": ["./src/utils/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
5. 建立標準目錄
# 建立專案結構
mkdir -p src/routes src/services src/middleware src/utils src/types
# 建立基礎檔案
touch src/app.ts
touch src/routes/index.ts
touch src/services/index.ts
touch src/middleware/index.ts
touch src/utils/index.ts
6. 建立基礎文件結構
src/
├── routes/ # 路由定義
│ ├── rest.ts # REST API 路由
│ ├── orpc.ts # ORPC 路由
│ └── index.ts
├── services/ # 業務邏輯服務
│ ├── otpService.ts # OTP 服務
│ ├── smsService.ts # 簡訊服務
│ └── index.ts
├── middleware/ # 中間件
│ ├── auth.ts # 驗證中間件
│ ├── validation.ts # 驗證中間件
│ ├── error.ts # 錯誤處理
│ └── index.ts
├── utils/ # 工具函數
│ ├── redis.ts # Redis 連接
│ ├── logger.ts # 日誌工具
│ └── index.ts
├── types/ # 本地型別定義
└── app.ts # 應用程式入口
7. 建立主要應用程式
// src/app.ts
import Fastify from 'fastify'
import cors from '@fastify/cors'
import helmet from '@fastify/helmet'
import rateLimit from '@fastify/rate-limit'
import { config } from './utils/config.js'
import { registerRoutes } from './routes/index.js'
const app = Fastify({
logger: {
level: config.LOG_LEVEL || 'info'
}
})
// 註冊插件
await app.register(helmet, {
contentSecurityPolicy: false
})
await app.register(cors, {
origin: config.CORS_ORIGIN || true
})
await app.register(rateLimit, {
max: 100,
timeWindow: '1 minute'
})
// 註冊路由
await app.register(registerRoutes)
// 健康檢查
app.get('/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() }
})
// 啟動伺服器
const start = async () => {
try {
await app.listen({
port: config.PORT || 8000,
host: '0.0.0.0'
})
app.log.info('🚀 Kyo-OTP Service started')
} catch (err) {
app.log.error(err)
process.exit(1)
}
}
start()
8. 建立配置管理
// src/utils/config.ts
import { config as dotenvConfig } from 'dotenv'
dotenvConfig()
export const config = {
// 伺服器配置
PORT: parseInt(process.env.PORT || '8000'),
NODE_ENV: process.env.NODE_ENV || 'development',
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
// CORS 配置
CORS_ORIGIN: process.env.CORS_ORIGIN || true,
// Redis 配置
REDIS_URL: process.env.REDIS_URL || 'redis://localhost:6379',
// 資料庫配置
DATABASE_URL: process.env.DATABASE_URL || '',
// 三竹簡訊配置
MITAKE_USERNAME: process.env.MITAKE_USERNAME || '',
MITAKE_PASSWORD: process.env.MITAKE_PASSWORD || '',
MITAKE_ENDPOINT: process.env.MITAKE_ENDPOINT || 'https://api.mitake.com.tw/api/sms',
// JWT 配置
JWT_SECRET: process.env.JWT_SECRET || 'your-secret-key',
// 速率限制
RATE_LIMIT_MAX: parseInt(process.env.RATE_LIMIT_MAX || '10'),
RATE_LIMIT_WINDOW: process.env.RATE_LIMIT_WINDOW || '1 minute'
} as const
export type Config = typeof config
9. 建立 Prisma 配置
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model OtpLog {
id String @id @default(cuid())
phone String
code String
status String // sent, verified, expired, failed
templateId Int?
msgId String?
createdAt DateTime @default(now())
@@map("otp_logs")
}
model OtpTemplate {
id Int @id @default(autoincrement())
name String
content String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("otp_templates")
}
10. 建立 Redis 連接
// src/utils/redis.ts
import { createClient } from 'redis'
import { config } from './config.js'
export const redis = createClient({
url: config.REDIS_URL
})
redis.on('error', (err) => {
console.error('Redis Client Error', err)
})
redis.on('connect', () => {
console.log('✅ Redis connected')
})
// 連接 Redis
await redis.connect()
11. 設定路由結構
// src/routes/index.ts
import type { FastifyInstance } from 'fastify'
import { restRoutes } from './rest.js'
import { orpcRoutes } from './orpc.js'
export async function registerRoutes(app: FastifyInstance) {
// 註冊 REST API 路由
await app.register(restRoutes, { prefix: '/api' })
// 註冊 ORPC 路由
await app.register(orpcRoutes, { prefix: '/orpc' })
}
12. 建立 REST API 路由
// src/routes/rest.ts
import type { FastifyInstance } from 'fastify'
export async function restRoutes(app: FastifyInstance) {
// OTP 發送
app.post('/otp/send', async (request, reply) => {
return { message: 'REST API: OTP Send endpoint' }
})
// OTP 驗證
app.post('/otp/verify', async (request, reply) => {
return { message: 'REST API: OTP Verify endpoint' }
})
// 發送記錄查詢
app.get('/otp/logs', async (request, reply) => {
return { message: 'REST API: OTP Logs endpoint' }
})
}
13. ESLint 配置
// .eslintrc.cjs
module.exports = {
root: true,
env: {
node: true,
es2022: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': 'error'
},
ignorePatterns: ['dist/', 'node_modules/']
}
14. 環境變數設定
# .env.example
# 伺服器配置
PORT=8000
NODE_ENV=development
LOG_LEVEL=info
# 資料庫配置
DATABASE_URL="postgresql://user:password@localhost:5432/kyo_otp"
REDIS_URL="redis://localhost:6379"
# 三竹簡訊配置
MITAKE_USERNAME=""
MITAKE_PASSWORD=""
MITAKE_ENDPOINT="https://api.mitake.com.tw/api/sms"
# JWT 配置
JWT_SECRET="your-secret-key"
# CORS 配置
CORS_ORIGIN="http://localhost:3000"
15. 測試開發環境
# 生成 Prisma Client
pnpm prisma generate
# 啟動開發伺服器
pnpm run dev
# 在另一個終端機測試建置
pnpm run build
# 檢查 TypeScript 編譯
pnpm exec tsc --noEmit
# 執行 linting
pnpm run lint
16. 檢查 Monorepo 整合
# 回到根目錄
cd ../../
# 測試 Turborepo 建置
pnpm run build
# 確認後端專案包含在 workspace 中
pnpm -r list --depth=0
17. 建立 OTP 服務骨架
// src/services/otpService.ts
import type { OtpSendRequest, OtpVerifyRequest } from '@kyong/kyo-types'
export class OtpService {
async send(request: OtpSendRequest) {
// TODO: 實作 OTP 發送邏輯
console.log('OTP Send:', request)
return { success: true, message: 'OTP sent successfully' }
}
async verify(request: OtpVerifyRequest) {
// TODO: 實作 OTP 驗證邏輯
console.log('OTP Verify:', request)
return { success: true, message: 'OTP verified successfully' }
}
private generateCode(length = 6): string {
return Math.random().toString(10).substring(2, 2 + length)
}
}
export const otpService = new OtpService()
✅ Fastify + TypeScript 後端專案初始化完成
✅ Prisma ORM 與資料庫 schema 設定
✅ Redis 連接與配置管理
✅ 基礎路由結構建立(REST + ORPC)
✅ 環境變數與配置管理系統
✅ ESLint 與開發工具設定
✅ Monorepo workspace 整合測試
Day4 我們會開始實作核心業務邏輯:OTP 生成與驗證服務,整合 Zod 驗證機制,以及建立 Redis 快取和速率限制功能。