iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Software Development

30 天打造工作室 SaaS 產品 (後端篇)系列 第 3

Day3:初始化後端專案與核心套件設定

  • 分享至 

  • xImage
  •  

為什麼要實作 OTP 驗證服務?

昨天我們設計了後端整體架構,今天要開始實作第一個微服務:Kyo-OTP 驗證服務。這個是新的客戶需求,也適合打造成微服務成為工作室SaaS的第一個業務服務。

  • 簡訊發送邏輯:整合不同的簡訊服務商 API
  • 驗證流程:OTP 生成、儲存、驗證、過期處理
  • 安全機制:防暴力破解、速率限制、帳號鎖定
  • 錯誤處理:統一的錯誤回應格式

OTP 服務可以解決這些重複性問題,成為所有專案的共用基礎設施。

Kyo-OTP 服務技術需求

核心業務邏輯

  • OTP 生成與管理:6位數驗證碼,5分鐘有效期
  • 簡訊發送:整合三竹簡訊 API,支援模板變數
  • 驗證與安全:3次錯誤鎖定,速率限制保護
  • 事件追蹤:完整的發送和驗證記錄

技術架構要求

  • 高可用性:Redis 快取 + PostgreSQL 持久化
  • 擴展性:水平擴展支援,無狀態設計
  • 監控性:完整的日誌和指標收集

建立後端服務基礎結構

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

設定 TypeScript 配置

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              # 應用程式入口

設定 Fastify 應用程式

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 快取和速率限制功能。


上一篇
Day2:Kyo-System 後端架構設計與微服務策略
系列文
30 天打造工作室 SaaS 產品 (後端篇)3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言