在Day15中,我們完成了PrismaORM與PostgreSQL的Schema設計、Migration建立,以及SeedData寫入。
然而,我們在Day14實作的Next.jsRouteHandlers(app/api/skills/route.ts)依然是用記憶體陣列(skillsDb)在儲存與過濾資料。一旦伺服器重啟或部署,新增與修改的技能資料就會消失。
今天(Day16),我們將正式進行RouteHandlers底層的記憶體陣列徹底移除,全面替換為PrismaClient的資料庫查詢,為VibePulse實現完整的CRUD(Create,Read,Update,Delete)資料持久化!
1.架構轉換:從MemoryMock到DatabasePersistence
重構前的RouteHandlers是從記憶體陣列進行filter與push;重構後,我們將交由PostgreSQL資料庫處理條件查詢、排序與分頁,發揮索引(Indexes)與ORM的威力:
重構前(In-MemoryArray):
Client--->APIHandler--->Array.filter()/Array.unshift()--->Response
重構後(Prisma+PostgreSQLPersistence):
Client--->APIHandler--->ZodValidation--->PrismaORM--->PostgreSQLDB
2.實戰步驟1:建立Enum轉換與MapHelper
由於PrismaSchema中的Enum採用大寫蛇形(FRONTEND,LEARNING),而前端介面與APIPayload採用CamelCase/PascalCase(Frontend,learning),我們需要一個雙向轉譯層,確保API規格與資料庫表層解耦。
開啟Cursor,對著@lib/utils/skillMappers.ts發送Prompt:
@prisma/schema.prisma (參考Prisma Enum定義)
請幫我在`lib/utils/skillMappers.ts`建立前端與Prisma DB格式之間的Mapping Helper函數。
## 需求細節:
1.`toDbCategory` / `fromDbCategory`**:
- 'Frontend' <-> 'FRONTEND'
- 'Backend' <-> 'BACKEND'
- 'AI' <-> 'AI'
- 'DevOps' <-> 'DEVOPS'
2.`toDbStatus` / `fromDbStatus`**:
- 'learning' <-> 'LEARNING'
- 'mastered' <-> 'MASTERED'
- 'planned' <-> 'PLANNED'
3.`formatSkillFromDb`**:將Prisma回傳的`Skill`DB物件轉譯為前端使用的JSON結構。
3.實戰步驟2:重構GET&POST(app/api/skills/route.ts)
現在,我們請AI把Day14的/app/api/skills/route.ts升級為PrismaORM版本。
發送Prompt對著@app/api/skills/route.ts:
@lib/prisma.ts
@lib/validations/skill.ts
@lib/api/response.ts
@lib/utils/skillMappers.ts
請幫我重構`app/api/skills/route.ts`中的`GET`與`POST`Handlers,將資料來源從記憶體陣列全面替換為`prisma.skill`!
需求說明:
1.`GET`端點(讀取技能列表):
-支援`category`(類別過濾)與`search`(關鍵字搜尋)。
-**Prisma查詢條件(`where`):
-若提供`category`,使用轉譯後的Enum進行精準比對。
-若提供`search`,使用`OR`條件搭配`title:{contains:search,mode:'insensitive'}`或`tags:{has:search}`查詢。
-**排序(`orderBy`)**:預設依`createdAt:'desc'`排序。
-將DB結果經由`formatSkillFromDb`格式化後,經由`successResponse`回傳。
2.`POST`端點(新增技能):
-使用Zod`createSkillSchema.safeParse(body)`驗證。
-驗證通過後,調用`prisma.skill.create()`寫入PostgreSQL。
-回傳`201Created`與新建構的技能資料。
-包含完整的`try-catch`例外處理與500錯誤攔截。
AI產出的app/api/skills/route.ts:
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { createSkillSchema } from '@/lib/validations/skill';
import { successResponse, errorResponse } from '@/lib/api/response';
import {
toDbCategory,
formatSkillFromDb,
} from '@/lib/utils/skillMappers';
import { Prisma } from '@prisma/client';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const search = searchParams.get('search')?.trim();
// 動態建構 Prisma Where 條件
const where: Prisma.SkillWhereInput = {};
if (category && category !== 'All') {
where.category = toDbCategory(category);
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ tags: { has: search } },
];
}
// 執行 DB 查詢
const dbSkills = await prisma.skill.findMany({
where,
orderBy: { createdAt: 'desc' },
});
const formattedSkills = dbSkills.map(formatSkillFromDb);
return successResponse(formattedSkills, 200, { total: formattedSkills.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;
// Prisma 寫入 DB
const newDbSkill = await prisma.skill.create({
data: {
title: validData.title,
category: toDbCategory(validData.category),
proficiency: validData.proficiency,
tags: validData.tags,
status: validData.status ? toDbStatus(validData.status) : 'LEARNING',
notesCount: validData.notesCount ?? 0,
},
});
return successResponse(formatSkillFromDb(newDbSkill), 201);
} catch (error) {
console.error('[POST /api/skills Error]:', error);
return errorResponse('伺服器處理請求時發生錯誤', 500);
}
}
4.實戰步驟3:新增單一技能動態路由PATCH&DELETE(app/api/skills/[id]/route.ts)
接下來,我們需要實作單一資源的操作:更新熟練度(PATCH)與刪除技能(DELETE)。
開啟Cursor,對著@app/api/skills/[id]/route.ts發送Prompt:
@lib/prisma.ts
@lib/validations/skill.ts
@lib/api/response.ts
@lib/utils/skillMappers.ts
請幫我在`app/api/skills/[id]/route.ts`實作動態路由的`PATCH`與`DELETE`Handlers。
需求說明:
1.`PATCH`端點(更新技能/熟練度):
-從`params.id`取得SkillID。
-先使用`prisma.skill.findUnique()`檢查資料是否存在,若不存在回傳`404NotFound`。
-使用`updateSkillSchema.safeParse(body)`進行PartialValidation。
-執行`prisma.skill.update()`,更新DB資料並回傳最新物件。
2.`DELETE`端點(刪除技能):
-檢查技能是否存在(404)。
-執行`prisma.skill.delete({where:{id}})`。
-回傳`200OK`訊息:`{message:"技能已成功刪除"}`。
AI產出的app/api/skills/[id]/route.ts:
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { updateSkillSchema } from '@/lib/validations/skill';
import { successResponse, errorResponse } from '@/lib/api/response';
import {
toDbCategory,
toDbStatus,
formatSkillFromDb,
} from '@/lib/utils/skillMappers';
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
// 檢查資源是否存在
const existingSkill = await prisma.skill.findUnique({ where: { id } });
if (!existingSkill) {
return errorResponse('找不到指定的技能項目', 404);
}
const body = await request.json();
const validationResult = updateSkillSchema.safeParse(body);
if (!validationResult.success) {
return errorResponse(
'更新資料格式不正確',
400,
validationResult.error.flatten().fieldErrors
);
}
const data = validationResult.data;
// 組裝更新 Data (僅更新有傳入的欄位)
const updatedDbSkill = await prisma.skill.update({
where: { id },
data: {
...(data.title && { title: data.title }),
...(data.category && { category: toDbCategory(data.category) }),
...(data.proficiency !== undefined && { proficiency: data.proficiency }),
...(data.tags && { tags: data.tags }),
...(data.status && { status: toDbStatus(data.status) }),
...(data.notesCount !== undefined && { notesCount: data.notesCount }),
lastPracticedAt: new Date(),
},
});
return successResponse(formatSkillFromDb(updatedDbSkill));
} catch (error) {
console.error(`[PATCH /api/skills/${params.id} Error]:`, error);
return errorResponse('更新技能失敗', 500);
}
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const existingSkill = await prisma.skill.findUnique({ where: { id } });
if (!existingSkill) {
return errorResponse('找不到指定的技能項目', 404);
}
await prisma.skill.delete({ where: { id } });
return successResponse({ message: '技能已成功刪除' }, 200);
} catch (error) {
console.error(`[DELETE /api/skills/${params.id} Error]:`, error);
return errorResponse('刪除技能失敗', 500);
}
}
今天我們成功替VibePulse完成了後端資料庫層級的最後拼圖:
建立了解耦Enum的Mapper工具,確保API層與DB結構清晰獨立。
將/api/skills的GET與POST正式升級為Prisma原生資料庫查詢與寫入。
建立了動態路由/api/skills/[id],實現了強健的PATCH(更新)與DELETE(刪除)邏輯。