บทที่ 4backend|20 KB

บทที่ 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ความหมายเมื่อไหร่ใช้
200OKRequest สำเร็จ
201Createdสร้างข้อมูลสำเร็จ
400Bad Requestข้อมูล input ไม่ถูกต้อง
401Unauthorizedไม่มี token หรือ token ไม่ถูกต้อง
403Forbiddenไม่มีสิทธิ์เข้าถึง
404Not Foundไม่พบข้อมูล
409Conflictข้อมูลซ้ำ (เช่น email ซ้ำ)
500Internal 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 สำหรับอัปโหลดไฟล์ (แยกประเภทไฟล์เสียงและรูปภาพ)

อธิบาย:

  1. Import Dependencies

    import { Router } from "express";
    
    • นำเข้า Router จาก Express เพื่อสร้าง sub-router
  2. Create Router Instance

    export const notesRouter = Router();
    
    • สร้าง router สำหรับ notes endpoints
  3. File Upload Setup

    const upload = makeUploader("audio");
    const imageUpload = makeUploader("image");
    
    • สร้าง middleware สำหรับ upload audio และ image
    • makeUploader() คือ helper function ที่ใช้ Multer
  4. Apply Authentication

    notesRouter.use(requireAuth);
    
    • ใช้ requireAuth middleware กับทุก 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:

  1. Route Definition

    notesRouter.post("/", async (req, res) => {
    
    • POST /api/notes - สร้าง note ใหม่
    • async เพราะต้องรอ Database operation
  2. Get User ID from Token

    userId: req.user._id,
    
    • req.user มาจาก requireAuth middleware
    • เก็บ user ID จาก JWT token
  3. Handle Optional Fields

    title: req.body.title || "บันทึกใหม่",
    language: req.body.language || "auto",
    
    • ถ้าไม่ส่ง title มา ใช้ "บันทึกใหม่"
    • ถ้าไม่ส่ง language มา ใช้ "auto"
  4. Validate Array

    tags: Array.isArray(req.body.tags) ? req.body.tags : [],
    
    • ตรวจสอบว่า tags เป็น array หรือไม่
    • ถ้าไม่ใช่ ใช้ empty array
  5. 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 });
});

อธิบาย:

  1. 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
  2. Build Filter

    const filter = { userId: req.user._id };
    
    • เริ่มจาก filter ด้วย userId (แสดงเฉพาะ notes ของ user นี้)
  3. Tag Filter

    if (tag) filter.tags = tag;
    
    • ถ้ามี tag parameter ให้กรองด้วย tag
  4. 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 });
});

อธิบาย:

  1. URL Parameters

    req.params.id
    
    • มาจาก URL path /api/notes/:id
    • ตัวอย่าง: /api/notes/65a1b2c3d4e5f6g7h8i9j0k1
  2. Populate (Join)

    .populate("userId", "displayName profile")
    
    • ดึงข้อมูล User ที่เกี่ยวข้อง
    • เอาเฉพาะ displayName และ profile fields
  3. 404 Handling

    if (!note) {
      return res.status(404).json({ 
        ok: false, 
        message: "Note not found" 
      });
    }
    
    • ถ้าไม่เจอ note ส่ง status 404
  4. 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 });
});

อธิบาย:

  1. Authorization Check

    const note = await Note.findOne({ 
      _id: req.params.id, 
      userId: req.user._id 
    });
    
    • ต้องเป็น note ของ user นี้เท่านั้น
    • ป้องกันการแก้ไข note ของคนอื่น
  2. Conditional Updates

    if (typeof title === "string") note.title = title;
    
    • แก้ไขเฉพาะ fields ที่ส่งมา
    • ตรวจสอบ type ก่อนแก้ไข
  3. 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 });
});

อธิบาย:

  1. 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 });
  }
);

อธิบาย:

  1. Multer Middleware

    upload.single("audio")
    
    • จัดการ file upload
    • single("audio"): รับ 1 ไฟล์จาก field ชื่อ "audio"
    • ไฟล์จะถูกเก็บใน req.file
  2. File Information

    req.file.filename  // ชื่อไฟล์ที่บันทึก
    req.file.mimetype  // MIME type (audio/mpeg, etc.)
    
  3. 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
  4. 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" 
    });
  }
});

อธิบาย:

  1. No Auth Check

    • ไม่ตรวจสอบ userId เพราะทุกคนแชร์ได้
    • แต่ยังต้องมี token (จาก requireAuth)
  2. 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" 
    });
  }
});

อธิบาย:

  1. Pagination

    const limit = parseInt(req.query.limit) || 20;
    const skip = parseInt(req.query.skip) || 0;
    
    • limit: จำนวนรายการต่อหน้า
    • skip: ข้ามกี่รายการ (สำหรับหน้าถัดไป)
  2. 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
  3. 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
}

📝 แบบฝึกหัด

คำถามทบทวน

  1. RESTful API คืออะไร?
  2. HTTP Method ไหนใช้สำหรับสร้างข้อมูล?
  3. Status Code 404 หมายความว่าอย่างไร?
  4. req.params และ req.query ต่างกันอย่างไร?
  5. ทำไมต้องตรวจสอบ 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

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 5: AI Integration (Google Gemini)
เราจะเรียนรู้การใช้ AI สำหรับสร้าง content, fallback strategies, และ caching


📚 แหล่งเรียนรู้เพิ่มเติม