iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Vibe Coding

Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地系列 第 14

Day 14|Next.js Route Handlers實作用Zod驗證Request Body與Error Handling

  • 分享至 

  • xImage
  •  

在Day13中,我們使用了Design-First的精神,請AI畫出了Mermaid時序圖,並制定了完整的RESTfulAPI規格書與統一Response格式。

有了這份規格後,今天我們將正式開始撰寫Next.jsAppRouter的RouteHandlers(app/api/...)。

在後端開發中,最危險的事情就是盲目信任前端傳來的資料。未經檢查的RequestBody可能導致資料庫欄位型別錯誤、寫入非法值(例如熟練度填了999或-50),甚至是潛在的安全漏洞。

今天,我們將引導AI使用Zod建立強型別驗證層,並實作具備統一錯誤處理(ErrorHandling)機制的Next.jsRouteHandlers!

1.為什麼選擇Zod作為API驗證層?
Zod是TypeScript生態系中最熱門的Schema宣告與驗證套件。它有三大核心優勢:

TypeScriptFirst:從ZodSchema可以直接用z.infer自動推導出TypeScript型別,無需重複定義。

運行時防禦(RuntimeProtection):在API接收到Payload的第一時間進行強型別與數值範圍檢查。

鏈式調用(Chainable Validation):輕鬆實現.min(), .max(), .email(), .trim()等商業邏輯約束。

2.實戰步驟1:安裝Zod與定義API驗證Schema
步驟A:安裝套件在Terminal執行:
npminstallzod
步驟B:建立ZodSchema
開啟Cursor,對著@lib/validations/skill.ts發送Prompt:
@docs/api-spec.md(參考Day13的API規格)

請幫我在lib/validations/skill.ts建立VibePulse技能模組的Zod驗證Schema。

需求細節:
1.createSkillSchema(新增技能):
-title:字串,長度限制2~50字,自動.trim()
-category:只能是'Frontend'|'Backend'|'AI'|'DevOps'枚舉。
-proficiency:數字,範圍必須在0到100之間。
-tags:字串陣列,預設空陣列[],最多5個Tag。
-status:'learning'|'mastered'|'planned',預設'learning'
2.updateSkillSchema(更新技能):
-使用createSkillSchema.partial()允許部分更新。
3.匯出TypeScript型別:匯出CreateSkillInputUpdateSkillInput型別。
AI產出的lib/validations/skill.ts:

import { z } from 'zod';

export const categoryEnum = z.enum(['Frontend', 'Backend', 'AI', 'DevOps']);
export const statusEnum = z.enum(['learning', 'mastered', 'planned']);

export const createSkillSchema = z.object({
  title: z
    .string({ required_error: '請輸入技能標題' })
    .min(2, '標題至少需要 2 個字')
    .max(50, '標題不能超過 50 個字')
    .trim(),
  category: categoryEnum,
  proficiency: z
    .number({ required_error: '請提供熟練度數值' })
    .min(0, '熟練度最低為 0')
    .max(100, '熟練度最高為 100'),
  tags: z.array(z.string()).max(5, '最多只能標記 5 個標籤').default([]),
  status: statusEnum.default('learning'),
  notesCount: z.number().min(0).default(0),
});

export const updateSkillSchema = createSkillSchema.partial();

export type CreateSkillInput = z.infer<typeof createSkillSchema>;
export type UpdateSkillInput = z.infer<typeof updateSkillSchema>;

3.實戰步驟2:建立統一APIResponseHelper(lib/api/response.ts)
為了落實Day13定的APIResponse規範,我們建立一個統一的回應包裝函數,確保所有API回傳格式一致:

// lib/api/response.ts
import { NextResponse } from 'next/server';

export function successResponse<T>(data: T, status = 200, meta?: Record<string, any>) {
  return NextResponse.json(
    {
      success: true,
      data,
      ...(meta && { meta }),
    },
    { status }
  );
}

export function errorResponse(message: string, status = 400, details?: any) {
  return NextResponse.json(
    {
      success: false,
      error: {
        code: status === 400 ? 'BAD_REQUEST' : status === 404 ? 'NOT_FOUND' : 'INTERNAL_ERROR',
        message,
        ...(details && { details }),
      },
    },
    { status }
  );
}

4.實戰步驟3:撰寫GET&POSTRouteHandler(app/api/skills/route.ts)
對著@app/api/skills/route.ts發送Prompt:

@lib/validations/skill.ts
@lib/api/response.ts
@mocks/skillsData.ts

請幫我在app/api/skills/route.ts實作Next.jsAppRouter的RouteHandlers(GETPOST)。

需求細節:
1.GET端點:
-支援QueryParameters:category(類別篩選)與search(標題/標籤關鍵字搜尋)。
-解析URL參數並過濾現有的mockSkills資料(目前先用記憶體/MockData,為Phase3DB做準備)。
-回傳successResponse(filteredSkills)

2.POST端點:
-讀取RequestJSONBody。
-使用createSkillSchema.safeParse(body)進行Zod驗證。
-若驗證失敗:回傳errorResponse('無效的輸入資料',400,result.error.flatten().fieldErrors)
-若驗證成功:組裝包含新id(例如sk-${Date.now()})與lastPracticedAt(當前ISO時間)的物件,寫入記憶體,並回傳successResponse(newSkill,201)
-外層需包覆try-catch捕捉未預期例外,回傳500InternalServerError
AI產出的app/api/skills/route.ts:

import { NextRequest } from 'next/server';
import { mockSkills, SkillItem } from '@/mocks/skillsData';
import { createSkillSchema } from '@/lib/validations/skill';
import { successResponse, errorResponse } from '@/lib/api/response';

// 記憶體中的技能陣列(模擬 DB)
let skillsDb: SkillItem[] = [...mockSkills];

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const category = searchParams.get('category');
    const search = searchParams.get('search')?.toLowerCase();

    let filtered = [...skillsDb];

    // 類別過濾
    if (category && category !== 'All') {
      filtered = filtered.filter((item) => item.category === category);
    }

    // 關鍵字搜尋
    if (search) {
      filtered = filtered.filter(
        (item) =>
          item.title.toLowerCase().includes(search) ||
          item.tags.some((tag) => tag.toLowerCase().includes(search))
      );
    }

    return successResponse(filtered, 200, { total: filtered.length });
  } catch (error) {
    console.error('[GET /api/skills Error]:', error);
    return errorResponse('無法取得技能列表', 500);
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // Zod 運行時嚴格驗證
    const validationResult = createSkillSchema.safeParse(body);

    if (!validationResult.success) {
      return errorResponse(
        '欄位驗證失敗,請檢查輸入內容',
        400,
        validationResult.error.flatten().fieldErrors
      );
    }

    const validData = validationResult.data;

    // 模擬資料庫寫入
    const newSkill: SkillItem = {
      id: `sk-${Date.now()}`,
      ...validData,
      lastPracticedAt: new Date().toISOString(),
    };

    skillsDb.unshift(newSkill);

    return successResponse(newSkill, 201);
  } catch (error) {
    console.error('[POST /api/skills Error]:', error);
    return errorResponse('伺服器處理請求時發生錯誤', 500);
  }
}
5. 測試驗證:使用 cURL 或 Postman 測試 API 邊界
寫完 API 後,我們來測試 Zod 是否真的發揮了防禦功能:
測試 1:傳送無效資料(熟練度 > 100)
curl -X POST http://localhost:3000/api/skills \
  -H "Content-Type: application/json" \
  -d '{"title": "X", "category": "Frontend", "proficiency": 150}'
API 嚴格攔截 (400 Bad Request):
{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "欄位驗證失敗,請檢查輸入內容",
    "details": {
      "title": ["標題至少需要 2 個字"],
      "proficiency": ["熟練度最高為 100"]
    }
  }
}

今天我們成功完成Next.jsRouteHandlers與Zod防禦層的建立:

建立了可推導TypeScript型別的ZodSchemas。

實作了統一ResponseHelper(successResponse,errorResponse)。

打造了符合RESTful規範且具備強大型別與數值界限防護的/api/skillsAPI端點。


上一篇
Day 13|後端API設計叫AI幫你畫Sequence Diagram與API規格
系列文
Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言