昨天我成功用 Prisma 操作資料庫,感覺就像魔法一樣 ✨
今天要更進一步,讓 Prisma、Express、TypeScript 真正「協作」,
打造出一個 型別安全又乾淨的 Todo API 專案架構。
今天要讓專案更有層次,建立幾個資料夾:
src/
├── prisma/
│ └── client.ts
├── dto/
│ ├── create-todo.dto.ts
│ └── update-todo.dto.ts
├── routes/
│ └── todo.routes.ts
├── middleware/
│ └── validate-dto.ts
├── index.ts
└── types/
└── index.d.ts
這樣之後維護起來更直覺,每個區塊只負責一件事。
src/prisma/client.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
這樣之後要用 Prisma 的地方只要:
import prisma from "../prisma/client";
沿用之前 class-validator 的寫法:
src/dto/create-todo.dto.ts
import { IsString, IsOptional, Length } from "class-validator";
export class CreateTodoDto {
@IsString()
@Length(1, 50)
task: string;
@IsOptional()
@IsString()
note?: string;
}
src/dto/update-todo.dto.ts
import { IsOptional, IsString, IsBoolean } from "class-validator";
export class UpdateTodoDto {
@IsOptional()
@IsString()
task?: string;
@IsOptional()
@IsBoolean()
done?: boolean;
@IsOptional()
@IsString()
note?: string;
}
src/middleware/validate-dto.ts
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
import { Request, Response, NextFunction } from "express";
export function validateDto(dtoClass: any) {
return async (req: Request, res: Response, next: NextFunction) => {
const instance = plainToInstance(dtoClass, req.body);
const errors = await validate(instance);
if (errors.length > 0) {
const messages = errors.map(err => Object.values(err.constraints || {})).flat();
return res.status(400).json({ errors: messages });
}
next();
};
}
src/routes/todo.routes.ts
import { Router } from "express";
import prisma from "../prisma/client";
import { validateDto } from "../middleware/validate-dto";
import { CreateTodoDto } from "../dto/create-todo.dto";
import { UpdateTodoDto } from "../dto/update-todo.dto";
const router = Router();
// GET 所有 Todo
router.get("/", async (req, res) => {
const todos = await prisma.todo.findMany();
res.json(todos);
});
// POST 新增 Todo
router.post("/", validateDto(CreateTodoDto), async (req, res) => {
const { task, note } = req.body;
const todo = await prisma.todo.create({ data: { task, note } });
res.status(201).json(todo);
});
// PUT 更新 Todo
router.put("/:id", validateDto(UpdateTodoDto), async (req, res) => {
const id = Number(req.params.id);
const { task, done, note } = req.body;
try {
const updated = await prisma.todo.update({
where: { id },
data: { task, done, note },
});
res.json(updated);
} catch {
res.status(404).json({ error: "找不到這筆 Todo" });
}
});
// DELETE 刪除 Todo
router.delete("/:id", async (req, res) => {
const id = Number(req.params.id);
try {
await prisma.todo.delete({ where: { id } });
res.json({ message: "Todo 已刪除" });
} catch {
res.status(404).json({ error: "找不到這筆 Todo" });
}
});
export default router;
src/index.ts
import "reflect-metadata";
import express from "express";
import todoRoutes from "./routes/todo.routes";
const app = express();
app.use(express.json());
app.use("/todos", todoRoutes);
app.listen(3000, () => {
console.log("🚀 Server running on http://localhost:3000");
});
新增一筆 Todo
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"task": "Day 25 測試 Prisma 整合", "note": "OK"}'
回傳:
{
"id": 1,
"task": "Day 25 測試 Prisma 整合",
"done": false,
"note": "OK",
"createdAt": "2025-10-10T10:00:00.000Z"
}
例如在編輯 router.get("/")
時,
輸入 prisma.todo.findMany({ where: { ... } })
TypeScript 會即時提示 Todo
的所有欄位(task、done、note 等),
這就是 Prisma + TS 最強大的地方——全自動型別安全 🧩
今天感覺就像正式組起了一個「乾淨、可維護的 API 架構」。
✨ 收穫整理:
以前寫 Express API 只求能跑,
今天的版本已經有明確架構、驗證與型別安全,
真的更接近「專業後端專案」的樣子。