昨日我們完成 Drizzle ORM 的資料庫 Schema 規劃,今天我們要為「AI 個人財務追蹤器」串接後端心臟——RESTful API。
我們會建立處理「新增交易 (Create Transaction)」與「獲取交易列表 (Get Transactions)」的 API 端點,透過分層架構 (Controller/Service Architecture) 讓 AI 產出清晰、高可維護性的 Node.js 程式碼。
在引導 AI 寫程式之前,我們先複習後端開發最經典的職責分離 (Separation of Concerns) 觀念:
req.body、req.query)、驗證參數,最後回傳對應的 HTTP Status Code (例如 200 OK, 201 Created, 400 Bad Request) 與 JSON response。我們在 IDE 中使用以下 Prompt 指示 AI 產生後端 API:
我們正在使用 Express.js 與 Drizzle ORM 開發財務追蹤器。
請基於 Day 6 的 schema.ts,幫我建立 Transaction (交易) 的 API 模組:
【需求】
1. 採用 Controller / Service 分層架構。
2. 實作兩個 API 端點:
- POST /api/transactions (新增交易)
- GET /api/transactions (取得特定使用者的交易列表)
3. 使用 TypeScript 撰寫,確保型別安全。
AI 很快地生成了 Service 與 Controller:
// transaction.service.ts (AI 初次生成版本)
import { db } from '../db';
import { transactions } from '../db/schema';
import { eq } from 'drizzle-orm';
export class TransactionService {
async createTransaction(userId: string, amount: number, type: 'INCOME' | 'EXPENSE', category: string) {
return await db.insert(transactions).values({
userId,
amount: amount.toString(), // 轉成 string 以符合 Drizzle numeric 欄位
type,
category,
}).returning();
}
async getUserTransactions(userId: string) {
return await db.select().from(transactions).where(eq(transactions.userId, userId));
}
}
然後
// transaction.controller.ts (AI 初次生成版本)
import { Request, Response } from 'express';
import { TransactionService } from './transaction.service';
const service = new TransactionService();
export const createTransactionHandler = async (req: Request, res: Response) => {
const { userId, amount, type, category } = req.body;
const result = await service.createTransaction(userId, amount, type, category);
res.status(201).json(result);
};
這段看似簡潔,但在真實的開發環境中,直接上線保證踩大雷!我檢查出了三個顯著問題:
createTransactionHandler 沒有使用 try-catch。如果資料庫連線中斷或欄位不符合規範,Express 會直接 Crash 或持續掛起(Hang 住了),最終導致前端收到 504 Timeout!async/await) 永遠必須考慮 Reject/Error 狀態。amount: -500 或空字串,Controller 照單全收直接寫入資料庫。400 Bad Request。numeric 映射為 string,Service 直接回傳原生的 DB 筆數,導致前端收到 { amount: "150.00" },前端如果不小心直接拿來加總,數值會變成字串串接!針對邊界條件與例外處理給出第二輪修正指令:
這段程式碼有安全與穩定度問題,請幫我重構:
1. 為 Controller 補上 try-catch 包覆,失敗時回傳 500 狀態碼與 JSON error message。
2. 在 Controller 新增基礎驗證:檢查 amount 是否大於 0、type 是否為合法 Enum。若驗證失敗回傳 400。
3. 在 Service 的 getUserTransactions 查詢中,回傳前將 amount 從 string 解析成 number (使用 Number(amount)),解決 JS 轉型問題。
// transaction.service.ts (最終優化版)
import { db } from '../db';
import { transactions } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export class TransactionService {
async createTransaction(data: { userId: string; amount: number; type: 'INCOME' | 'EXPENSE'; category: string }) {
const [inserted] = await db.insert(transactions).values({
userId: data.userId,
amount: data.amount.toString(),
type: data.type,
category: data.category,
}).returning();
return {
...inserted,
amount: Number(inserted.amount) // 轉換回數字型別
};
}
async getUserTransactions(userId: string) {
const list = await db.select()
.from(transactions)
.where(eq(transactions.userId, userId))
.orderBy(desc(transactions.date));
// 格式化輸出,確保 amount 為數字
return list.map(item => ({
...item,
amount: Number(item.amount)
}));
}
}
// transaction.controller.ts (最終優化版)
import { Request, Response } from 'express';
import { TransactionService } from './transaction.service';
const service = new TransactionService();
export const createTransactionHandler = async (req: Request, res: Response) => {
try {
const { userId, amount, type, category } = req.body;
// 1. 邊界條件與輸入驗證
if (!userId || !amount || !type || !category) {
return res.status(400).json({ error: 'Missing required fields' });
}
if (typeof amount !== 'number' || amount <= 0) {
return res.status(400).json({ error: 'Amount must be a positive number' });
}
if (!['INCOME', 'EXPENSE'].includes(type)) {
return res.status(400).json({ error: 'Invalid transaction type' });
}
// 2. 呼叫 Service 處理商業邏輯
const result = await service.createTransaction({ userId, amount, type, category });
return res.status(201).json({ success: true, data: result });
} catch (error) {
console.error('[CreateTransaction Error]:', error);
return res.status(500).json({ error: 'Internal server error' });
}
};
需要認真審視「不快樂的路徑 (Unhappy Path)」:
當資料格式錯了怎麼辦?伺服器崩潰怎麼辦?型別轉換隱患在哪裡?
將驗證邏輯與 Try-Catch 防護罩補上後,API 才能從「能運作的 Demo」晉升為「能承受真實流量的 Production 代碼」~~~