iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Vibe Coding

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

# Day 9 : 別讓防線大開!引導 AI 補足 Try-Catch、Zod 驗證與全域 Error Handling

  • 分享至 

  • xImage
  •  

今日目標

到了 Day 9,我們的「AI 個人財務追蹤器」已經擁有 Auth 機制與 API。但大部分 AI 預設產出的程式碼都有個致命缺點:極度樂觀

預設狀況下,AI 往往只考慮使用者「輸入正確金額、正確日期」的快樂路徑 (Happy Path)。只要前端傳來 -9999 金額、未定義的 category,或是帶有 HTML 標籤的防護攻擊字串,伺服器就會直接爆掉(回傳 500 Internal Server Error),甚至寫入污染資料!

今天我們要開啟 Vibe Mode,引導 AI 為 API 加上 Zod 強型別 Schema 驗證全域 Try-Catch / Exception Filter


Human vs. AI 實戰對照:新增交易紀錄 (Create Transaction) API

1. 給 AI 的指令 (Prompt)

我們正在開發 Next.js 14 App Router 專案中的「新增交易紀錄 API」(/api/transactions)。
請幫我撰寫 API Route Handlers (POST 方法):
1. 使用 Zod 驗證前端傳入的 Request Body。
2. 欄位包含:amount (正數數值)、type ('income' | 'expense')、category (字串)、date (ISO 日期格式)。
3. 加入完整的 Try-Catch 與錯誤處理,當驗證失敗時回傳 400,伺服器異常回傳 500。

2. AI 產出的結果 (Code Snippet)
AI 很快產出了以下程式碼:
// app/api/transactions/route.ts
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createClient } from '@/utils/supabase/server'

const transactionSchema = z.object({
  amount: z.number().positive(),
  type: z.enum(['income', 'expense']),
  category: z.string(),
  date: z.string(),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const validatedData = transactionSchema.parse(body)

    const supabase = createClient()
    const { data, error } = await supabase
      .from('transactions')
      .insert(validatedData)

    if (error) throw error

    return NextResponse.json({ success: true, data }, { status: 201 })
  } catch (error) {
    return NextResponse.json({ error: (error as Error).message }, { status: 400 })
  }
}

3. 做出的修正與判斷 (Human Touch)

雖然這段程式碼看似有用了 Zod 和 Try-Catch,但有三個防禦性漏洞:

 * 混淆 400 與 500 錯誤:在 catch 區塊中,AI 將「所有」捕捉到的錯誤(不論是 Zod 驗證失敗還是 Supabase 連線中斷)通通統一回傳 status: 400。資料庫斷線應該是 500 Server Error,而不是用戶的錯!
 * 型別強制轉換漏洞:前端表單傳送過來的金額有時是字串 "150",z.number().positive() 會直接噴錯,體驗極差。應該使用 z.coerce.number() 進行彈性轉型。
 * 敏感資訊外洩:如果 Supabase 噴出資料庫內部錯誤訊息(例如 SQL constraint 衝突),直接暴露給前端會造成資安漏洞。
💥 翻車與除錯過程 (Debug Experience)
🐛 踩坑:z.parse() 直接拋出 Unhandled Exception 導致日誌失控
當前端傳入不合規的資料(例如空物件 {})時,transactionSchema.parse() 會拋出 ZodError。因為 AI 寫的程式碼直接把它轉成字串暴露給前端:
{
  "error": "[\n  {\n    \"code\": \"invalid_type\",\n    \"expected\": \"number\",\n    \"received\": \"undefined\",\n    \"path\": [\n      \"amount\"\n    ],\n    \"message\": \"Required\"\n  }\n]"
}

前端收到這一長串 Raw JSON 字串完全無法直接呈現給 User,且伺服器 Console 被滿滿的預期內驗證錯誤洗版!
我的引導修正 (Secondary Prompt)
我把這個回應結構丟回給 AI,要求重構 Zod 的驗證邏輯與錯誤轉化器:
你產出的 catch 區塊問題很大:

  1. 不要用 .parse(),改用 safeParse() 來優化效能與流程控制。
  2. 請區分 ZodError 與一般的 Error。如果是 ZodError,請格式化為簡單的 key-value 錯誤訊息(例如 { fields: { amount: 'Required' } })並回傳 400。
  3. 如果是未知的 Server Error,記錄 log 後回傳簡短的 "Internal Server Error" 並給予 500 狀態碼。

最終修復後的 API Handler

// app/api/transactions/route.ts
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { createClient } from '@/utils/supabase/server'

const transactionSchema = z.object({
  amount: z.coerce.number().positive('金額必須大於 0'),
  type: z.enum(['income', 'expense'], { errorMap: () => ({ message: '類型必須為收入或支出' }) }),
  category: z.string().min(1, '請選擇分類'),
  date: z.string().datetime({ message: '無效的 ISO 日期格式' }),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()
    
    // 1. 使用 safeParse 避免拋出例外,優化控制流
    const validationResult = transactionSchema.safeParse(body)

    if (!validationResult.success) {
      // 提取並格式化欄位錯誤訊息
      const fieldErrors = validationResult.error.flatten().fieldErrors
      return NextResponse.json(
        { success: false, errors: fieldErrors },
        { status: 400 }
      )
    }

    const supabase = createClient()
    const { data, error } = await supabase
      .from('transactions')
      .insert(validationResult.data)
      .select()

    if (error) {
      console.error('[Database Error]:', error)
      return NextResponse.json(
        { success: false, message: '資料庫寫入失敗' },
        { status: 500 }
      )
    }

  return NextResponse.json({ success: true, data }, { status: 201 })

  } catch (err) {
    console.error('[Unhandled API Error]:', err)
    return NextResponse.json(
      { success: false, message: '伺服器內部發生未知錯誤' },
      { status: 500 }
    )
  }
}

JavaScript / TS 概念小知識

  • parse() vs safeParse():
    在 Zod 中,parse() 在驗證失敗時會直接 throw 一個例外,中斷程式碼執行;而 safeParse() 會回傳一個帶有 { success: boolean, data / error } 的 Discriminated Union 物件。在 API 開發中使用 safeParse() 可以讓防護邊界更加明確且易於追蹤。
  • Error Boundaries & Custom Error Mapping:
    JavaScript 的 try...catch 區塊捕捉到的 catch(error) 型別在 TypeScript 中預設是 unknown。工程師不能直接假設它有 .message 屬性,必須透過 instanceof ZodError 或條件判斷進行 Type Guard(型別守衛),這正是防止 Node.js 進程無故崩潰(Crash)的核心手段。

結語

Vibe Coding 的最高境界不是程式碼寫得多快,而是你能不能預先看見 AI 的盲點。
寫出 Happy Path 只需要 10 秒,但加入嚴密的 Validation 與 Try-Catch 才決定了你的專案是一張一撕就破的紙,還座堅固的堡壘。
明天 Day 10,我們要運用 AI 幫我們自動編寫 API 單元測試與整合測試 (Vitest/Jest),驗證這些邊界條件是否真的萬無一失!


上一篇
# Day 8 : 使用者是誰?整合 Supabase Auth 輕鬆搞定身份驗證與授權
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言