บทที่ 4: API Routes และ CRUD Operations
สารบัญเนื้อหา
บทที่ 4: API Routes และ CRUD Operations
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจ RESTful API Design Principles
- สร้าง CRUD Operations (Create, Read, Update, Delete)
- ใช้ HTTP Methods และ Status Codes อย่างถูกต้อง
- จัดการ Query Parameters และ Request Body
- ใช้ Middleware สำหรับ Authentication
- จัดการ File Uploads ด้วย Multer
📚 ทฤษฎีและแนวคิด
RESTful API คืออะไร?
REST (Representational State Transfer) คือ architectural style สำหรับออกแบบ Web APIs
หลักการสำคัญ:
- 🔹 Resource-based: ทุกอย่างเป็น resource (users, notes, etc.)
- 🔹 HTTP Methods: ใช้ GET, POST, PUT/PATCH, DELETE
- 🔹 Stateless: แต่ละ request ต้องมีข้อมูลครบ (ใช้ JWT)
- 🔹 JSON Format: ส่งข้อมูลเป็น JSON
HTTP Methods และการใช้งาน
| Method | การใช้งาน | ตัวอย่าง |
|---|---|---|
| GET | อ่านข้อมูล | GET /api/notes - ดึง notes ทั้งหมด |
| POST | สร้างข้อมูลใหม่ | POST /api/notes - สร้าง note ใหม่ |
| PATCH | แก้ไขบางส่วน | PATCH /api/notes/:id - แก้ไข note |
| PUT | แก้ไขทั้งหมด | PUT /api/notes/:id - replace note |
| DELETE | ลบข้อมูล | DELETE /api/notes/:id - ลบ note |
HTTP Status Codes
| Code | ความหมาย | เมื่อไหร่ใช้ |
|---|---|---|
| 200 | OK | Request สำเร็จ |
| 201 | Created | สร้างข้อมูลสำเร็จ |
| 400 | Bad Request | ข้อมูล input ไม่ถูกต้อง |
| 401 | Unauthorized | ไม่มี token หรือ token ไม่ถูกต้อง |
| 403 | Forbidden | ไม่มีสิทธิ์เข้าถึง |
| 404 | Not Found | ไม่พบข้อมูล |
| 409 | Conflict | ข้อมูลซ้ำ (เช่น email ซ้ำ) |
| 500 | Internal Server Error | เกิด error ที่ server |
💻 โค้ดจริงจาก QUOM App
1. Router Setup และ Middleware
ไฟล์: backend/src/routes/notes.js
import { Router } from "express";
import { requireAuth } from "../middleware/auth.js";
import { Note } from "../models/Note.js";
import { makeUploader } from "../utils/upload.js";
export const notesRouter = Router();
const upload = makeUploader("audio");
const imageUpload = makeUploader("image");
// ✅ Apply auth middleware to ALL routes
notesRouter.use(requireAuth);
อธิบายทีละบรรทัด (Imports):
-
import { Router } from "express"- นำเข้า Router เพื่อสร้าง Group ของ API endpoints (เช่น /api/notes/*)
-
import { requireAuth } from "../middleware/auth.js"- นำเข้า Middleware ที่เราสร้างเอง เพื่อเช็คว่า User ล็อกอินหรือยัง
-
import { Note } from "../models/Note.js"- นำเข้า Model ของ Note เพื่อใช้ดึง/สร้าง/แก้ไขข้อมูล
-
createUploader("audio")/createUploader("image")- สร้าง function สำหรับอัปโหลดไฟล์ (แยกประเภทไฟล์เสียงและรูปภาพ)
อธิบาย:
-
Import Dependencies
import { Router } from "express";- นำเข้า Router จาก Express เพื่อสร้าง sub-router
-
Create Router Instance
export const notesRouter = Router();- สร้าง router สำหรับ notes endpoints
-
File Upload Setup
const upload = makeUploader("audio"); const imageUpload = makeUploader("image");- สร้าง middleware สำหรับ upload audio และ image
makeUploader()คือ helper function ที่ใช้ Multer
-
Apply Authentication
notesRouter.use(requireAuth);- ใช้
requireAuthmiddleware กับทุก route - ทุก request ต้องมี valid JWT token
- ใช้
2. CREATE - สร้าง Note ใหม่
// Create note
notesRouter.post("/", async (req, res) => {
const note = await Note.create({
userId: req.user._id,
title: req.body.title || "บันทึกใหม่",
language: req.body.language || "auto",
tags: Array.isArray(req.body.tags) ? req.body.tags : [],
});
res.json({ ok: true, note });
});
อธิบาย Step-by-Step:
-
Route Definition
notesRouter.post("/", async (req, res) => {POST /api/notes- สร้าง note ใหม่asyncเพราะต้องรอ Database operation
-
Get User ID from Token
userId: req.user._id,req.userมาจากrequireAuthmiddleware- เก็บ user ID จาก JWT token
-
Handle Optional Fields
title: req.body.title || "บันทึกใหม่", language: req.body.language || "auto",- ถ้าไม่ส่ง title มา ใช้ "บันทึกใหม่"
- ถ้าไม่ส่ง language มา ใช้ "auto"
-
Validate Array
tags: Array.isArray(req.body.tags) ? req.body.tags : [],- ตรวจสอบว่า tags เป็น array หรือไม่
- ถ้าไม่ใช่ ใช้ empty array
-
Send Response
res.json({ ok: true, note });- ส่ง note ที่สร้างกลับไป
- Status code default คือ 200
3. READ - อ่านข้อมูล
3.1 Get All Notes (with Search)
// List notes
notesRouter.get("/", async (req, res) => {
const q = (req.query.q || "").toString().trim();
const tag = (req.query.tag || "").toString().trim();
const filter = { userId: req.user._id };
if (tag) filter.tags = tag;
if (q) {
filter.$or = [
{ title: new RegExp(q, "i") },
{ transcript: new RegExp(q, "i") },
{ "quote.text": new RegExp(q, "i") },
];
}
const notes = await Note.find(filter).sort({ createdAt: -1 }).limit(100);
res.json({ ok: true, notes });
});
อธิบาย:
-
Query Parameters
const q = (req.query.q || "").toString().trim(); const tag = (req.query.tag || "").toString().trim();req.queryมาจาก URL query string- ตัวอย่าง:
/api/notes?q=success&tag=motivation
-
Build Filter
const filter = { userId: req.user._id };- เริ่มจาก filter ด้วย userId (แสดงเฉพาะ notes ของ user นี้)
-
Tag Filter
if (tag) filter.tags = tag;- ถ้ามี tag parameter ให้กรองด้วย tag
-
Search Filter
if (q) { filter.$or = [ { title: new RegExp(q, "i") }, { transcript: new RegExp(q, "i") }, { "quote.text": new RegExp(q, "i") }, ];
}
- `$or`: ค้นหาใน field ใดก็ได้
- `new RegExp(q, "i")`: Regular Expression, `i` = case-insensitive
- ค้นหาใน title, transcript, หรือ quote.text
5. **Execute Query**
```javascript
const notes = await Note.find(filter)
.sort({ createdAt: -1 })
.limit(100);
.sort({ createdAt: -1 }): เรียงจากใหม่ไปเก่า.limit(100): จำกัดไม่เกิน 100 รายการ
3.2 Get Single Note by ID
// Get note by id
notesRouter.get("/:id", async (req, res) => {
const note = await Note.findOne({ _id: req.params.id })
.populate("userId", "displayName profile");
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
// Increment view count
note.viewCount = (note.viewCount || 0) + 1;
await note.save();
res.json({ ok: true, note });
});
อธิบาย:
-
URL Parameters
req.params.id- มาจาก URL path
/api/notes/:id - ตัวอย่าง:
/api/notes/65a1b2c3d4e5f6g7h8i9j0k1
- มาจาก URL path
-
Populate (Join)
.populate("userId", "displayName profile")- ดึงข้อมูล User ที่เกี่ยวข้อง
- เอาเฉพาะ
displayNameและprofilefields
-
404 Handling
if (!note) { return res.status(404).json({ ok: false, message: "Note not found" }); }- ถ้าไม่เจอ note ส่ง status 404
-
View Count
note.viewCount = (note.viewCount || 0) + 1; await note.save();- เพิ่ม view count ทุกครั้งที่ดู note
4. UPDATE - แก้ไขข้อมูล
4.1 Update Basic Fields
// Update note fields (title/tags/language)
notesRouter.patch("/:id", async (req, res) => {
const note = await Note.findOne({
_id: req.params.id,
userId: req.user._id
});
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
const { title, tags, language } = req.body || {};
if (typeof title === "string") note.title = title;
if (Array.isArray(tags)) note.tags = tags.map((t) => String(t));
if (typeof language === "string") note.language = language;
await note.save();
res.json({ ok: true, note });
});
อธิบาย:
-
Authorization Check
const note = await Note.findOne({ _id: req.params.id, userId: req.user._id });- ต้องเป็น note ของ user นี้เท่านั้น
- ป้องกันการแก้ไข note ของคนอื่น
-
Conditional Updates
if (typeof title === "string") note.title = title;- แก้ไขเฉพาะ fields ที่ส่งมา
- ตรวจสอบ type ก่อนแก้ไข
-
Array Sanitization
if (Array.isArray(tags)) note.tags = tags.map((t) => String(t));- แปลง elements ทั้งหมดเป็น String
- ป้องกัน type mismatch
4.2 Update Transcript
// Update transcript
notesRouter.patch("/:id/transcript", async (req, res) => {
const note = await Note.findOne({
_id: req.params.id,
userId: req.user._id
});
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
const text = (req.body.transcript || "").toString();
if (!text.trim()) {
return res.status(400).json({
ok: false,
message: "transcript required"
});
}
note.transcript = text;
await note.save();
res.json({ ok: true, note });
});
อธิบาย:
- Validation
if (!text.trim()) { return res.status(400).json({ ok: false, message: "transcript required" }); }- ตรวจสอบว่า transcript ไม่ว่าง
- Status 400 = Bad Request
5. File Upload Endpoints
5.1 Upload Audio
// Upload audio to a note
notesRouter.post("/:id/upload-audio",
upload.single("audio"),
async (req, res) => {
const note = await Note.findOne({
_id: req.params.id,
userId: req.user._id
});
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
const base = process.env.BASE_URL ||
`http://localhost:${process.env.PORT || 4010}`;
const url = `${base}/uploads/${req.file.filename}`;
note.audio = {
fileName: req.file.filename,
url,
mimeType: req.file.mimetype || "",
durationMs: Number(req.body.durationMs || 0),
};
await note.save();
res.json({ ok: true, note });
}
);
อธิบาย:
-
Multer Middleware
upload.single("audio")- จัดการ file upload
single("audio"): รับ 1 ไฟล์จาก field ชื่อ "audio"- ไฟล์จะถูกเก็บใน
req.file
-
File Information
req.file.filename // ชื่อไฟล์ที่บันทึก req.file.mimetype // MIME type (audio/mpeg, etc.) -
Build URL
const base = process.env.BASE_URL || `http://localhost:${process.env.PORT || 4010}`; const url = `${base}/uploads/${req.file.filename}`;- สร้าง URL สำหรับเข้าถึงไฟล์
- ตัวอย่าง:
https://api.quom.com/uploads/audio-123.mp3
-
Save to Database
note.audio = { fileName: req.file.filename, url, mimeType: req.file.mimetype || "", durationMs: Number(req.body.durationMs || 0), };- เก็บข้อมูลไฟล์ใน note
6. Special Endpoints
6.1 Increment Share Count
// Increment share count
notesRouter.post("/:id/share", async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
note.shareCount = (note.shareCount || 0) + 1;
await note.save();
res.json({ ok: true, shareCount: note.shareCount });
} catch (error) {
console.error("Share count error:", error);
res.status(500).json({
ok: false,
message: "Failed to update share count"
});
}
});
อธิบาย:
-
No Auth Check
- ไม่ตรวจสอบ
userIdเพราะทุกคนแชร์ได้ - แต่ยังต้องมี token (จาก
requireAuth)
- ไม่ตรวจสอบ
-
Atomic Increment
note.shareCount = (note.shareCount || 0) + 1;- ป้องกัน undefined
- เพิ่มทีละ 1
6.2 Get Popular Quotes
// Get popular quotes (most favorited)
notesRouter.get("/popular", async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 20;
const skip = parseInt(req.query.skip) || 0;
const { Favorite } = await import("../models/Favorite.js");
// Aggregate favorites count per note
const favCounts = await Favorite.aggregate([
{ $group: { _id: "$noteId", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: limit + skip },
{ $skip: skip }
]);
const noteIds = favCounts.map(f => f._id);
const notes = await Note.find({ _id: { $in: noteIds } })
.populate("userId", "displayName profile")
.lean();
// Attach favorite count and calculate score
const notesWithCount = notes.map(note => {
const fav = favCounts.find(
f => f._id.toString() === note._id.toString()
);
const favorites = fav?.count || 0;
const shares = note.shareCount || 0;
const views = note.viewCount || 0;
// Score = Favorites*2 + Shares*3 + Views*1
const score = (favorites * 2) + (shares * 3) + views;
return {
...note,
favoriteCount: favorites,
score
};
});
// Sort by calculated score
notesWithCount.sort((a, b) => b.score - a.score);
res.json({ ok: true, notes: notesWithCount });
} catch (error) {
console.error("Popular quotes error:", error);
res.status(500).json({
ok: false,
message: "Failed to fetch popular quotes"
});
}
});
อธิบาย:
-
Pagination
const limit = parseInt(req.query.limit) || 20; const skip = parseInt(req.query.skip) || 0;limit: จำนวนรายการต่อหน้าskip: ข้ามกี่รายการ (สำหรับหน้าถัดไป)
-
MongoDB Aggregation
const favCounts = await Favorite.aggregate([ { $group: { _id: "$noteId", count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: limit + skip }, { $skip: skip } ]);$group: นับจำนวน favorites ต่อ note$sort: เรียงจากมากไปน้อย$limit+$skip: Pagination
-
Popularity Score
const score = (favorites * 2) + (shares * 3) + views;- Shares มีน้ำหนักมากที่สุด (x3)
- Favorites รองลงมา (x2)
- Views น้อยที่สุด (x1)
🔄 RESTful API Design Patterns
URL Naming Conventions
✅ ดี
GET /api/notes # List all notes
POST /api/notes # Create note
GET /api/notes/:id # Get single note
PATCH /api/notes/:id # Update note
DELETE /api/notes/:id # Delete note
❌ ไม่ดี
GET /api/getNotes
POST /api/createNote
GET /api/note?id=123
Response Format
// ✅ Success Response
{
"ok": true,
"note": { ... },
"message": "Optional success message"
}
// ❌ Error Response
{
"ok": false,
"message": "Error description",
"errors": [ ... ] // Optional validation errors
}
📝 แบบฝึกหัด
คำถามทบทวน
- RESTful API คืออะไร?
- HTTP Method ไหนใช้สำหรับสร้างข้อมูล?
- Status Code 404 หมายความว่าอย่างไร?
req.paramsและreq.queryต่างกันอย่างไร?- ทำไมต้องตรวจสอบ
userIdก่อนแก้ไข note?
โจทย์ปฏิบัติ
โจทย์ที่ 1: สร้าง DELETE Endpoint
// TODO: สร้าง DELETE /api/notes/:id
// - ตรวจสอบว่าเป็น note ของ user นี้
// - ลบ note
// - ส่ง response { ok: true }
โจทย์ที่ 2: สร้าง Search Endpoint
// TODO: สร้าง GET /api/notes/search
// - รับ query parameter: q (search term)
// - ค้นหาใน title และ content
// - Support pagination (limit, skip)
โจทย์ที่ 3: สร้าง Bulk Update
// TODO: สร้าง PATCH /api/notes/bulk
// - รับ array ของ note IDs
// - Update tags สำหรับทุก notes
// - ส่ง count ของ notes ที่ update
📌 สรุป
ในบทนี้เราได้เรียนรู้:
✅ RESTful API design principles
✅ CRUD Operations สำหรับ Notes
✅ HTTP Methods และ Status Codes
✅ Query Parameters สำหรับ search และ filter
✅ File Upload ด้วย Multer
✅ Authorization ตรวจสอบ ownership
✅ Aggregation สำหรับ statistics
ไฟล์ที่เกี่ยวข้อง
backend/src/routes/notes.js- Notes routesbackend/src/models/Note.js- Note model
🔜 บทถัดไป
บทที่ 5: AI Integration (Google Gemini)
เราจะเรียนรู้การใช้ AI สำหรับสร้าง content, fallback strategies, และ caching