iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 7

# Day 7 : Vibe Mode 開啟:打造 RESTful API —— 讓 AI 撰寫 Controller 與 Service 邏輯

  • 分享至 

  • xImage
  •  

本日目標

昨日我們完成 Drizzle ORM 的資料庫 Schema 規劃,今天我們要為「AI 個人財務追蹤器」串接後端心臟——RESTful API

我們會建立處理「新增交易 (Create Transaction)」與「獲取交易列表 (Get Transactions)」的 API 端點,透過分層架構 (Controller/Service Architecture) 讓 AI 產出清晰、高可維護性的 Node.js 程式碼。


JavaScript 觀念補給站:Controller vs. Service

在引導 AI 寫程式之前,我們先複習後端開發最經典的職責分離 (Separation of Concerns) 觀念:

  1. Controller (控制器):負責 HTTP 層面的事。解析 Request(如 req.bodyreq.query)、驗證參數,最後回傳對應的 HTTP Status Code (例如 200 OK, 201 Created, 400 Bad Request) 與 JSON response。
  2. Service (服務層):負責核心業務邏輯與資料庫互動。不關心 HTTP 請求從何而來,只專注於處理商業邏輯(例如計算財務總額、呼叫 Drizzle 存取 DB)。

Human vs. AI 實戰對決

1. 我給 AI 的指令 (Prompt)

我們在 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 撰寫,確保型別安全。

2. AI 產出的結果 (AI Output)

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);
};

3. 我做出的修正與翻車記錄 (Debug & Human Intervention)

這段看似簡潔,但在真實的開發環境中,直接上線保證踩大雷!我檢查出了三個顯著問題:

🚨 翻車點 1:完全沒有 Error Handling(毀滅性崩潰風險)

  • 問題createTransactionHandler 沒有使用 try-catch。如果資料庫連線中斷或欄位不符合規範,Express 會直接 Crash 或持續掛起(Hang 住了),最終導致前端收到 504 Timeout!
  • JS 觀念:非同步操作 (async/await) 永遠必須考慮 Reject/Error 狀態。

🚨 翻車點 2:欠缺 HTTP 狀態碼與邊界檢查

  • 問題:如果前端傳入 amount: -500 或空字串,Controller 照單全收直接寫入資料庫。
  • 解決方案:邊界條件檢查應該在 Controller 第一時間擋下,回傳 400 Bad Request

🚨 翻車點 3:前端拿到的 amount 型別是字串

  • 問題:Day 6 我們提到 Drizzle 將 numeric 映射為 string,Service 直接回傳原生的 DB 筆數,導致前端收到 { amount: "150.00" },前端如果不小心直接拿來加總,數值會變成字串串接!

引導 AI 修正 (The Refined Prompt)

針對邊界條件與例外處理給出第二輪修正指令:

這段程式碼有安全與穩定度問題,請幫我重構:
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 代碼」~~~


上一篇
# Day 6: Vibe Mode 開啟:資料庫設計 —— 用 AI 規劃 ERD、生成 SQL Migration 與 Drizzle Schema
下一篇
# Day 8 : 使用者是誰?整合 Supabase Auth 輕鬆搞定身份驗證與授權
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言