在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型別:匯出CreateSkillInput與UpdateSkillInput型別。
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(GET與POST)。
需求細節:
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端點。