
กำลังโหลด...

กำลังโหลด...

ทีม Frontend รออยู่แล้ว เขาจะเอา API นี้ไปต่อเลย ดังนั้นขอของที่เชื่อถือได้
เงื่อนไขคือ ต้องผ่าน Test Checklist ทั้ง 13 ข้อ ไม่ใช่แค่ "ทางที่ถูกต้อง" ทำงานได้ ทางที่ผิดก็ต้องตอบให้ถูกด้วย เพราะ Frontend เขาต้องเอา Status Code ไปแสดงผลให้ผู้ใช้
เป้าหมายของ Boss นี้: ประกอบ CRUD ทั้ง 4 เข้าด้วยกันเป็น API เดียวที่สมบูรณ์ และทดสอบ ครบทุกเส้นทางด้วยตัวเอง
จบ Boss นี้คุณจะ
Books API (Chapter 3 ฉบับสมบูรณ์)
+--------+---------------+----------------------+--------------------------+
| Method | Path | สำเร็จ | ล้มเหลว |
+--------+---------------+----------------------+--------------------------+
| GET | /books | 200 + Array | - |
| GET | /books/:id | 200 + Object | 404 ถ้าไม่เจอ |
| POST | /books | 201 + Object ที่สร้าง | 400 ถ้าข้อมูลไม่ครบ |
| PUT | /books/:id | 200 + Object ที่แก้ | 404 ไม่เจอ / 400 ไม่ครบ |
| DELETE | /books/:id | 200 + Object ที่ลบ | 404 ถ้าไม่เจอ |
+--------+---------------+----------------------+--------------------------+
flowchart LR
C["Thunder Client<br/>/ Postman"] -->|"HTTP"| E["Express :3000"]
E --> M["express.json()"]
M --> R["Routes"]
R -->|"readBooks()"| A[("data/books.json")]
R -->|"writeBooks()"| A
A -.->|"ข้อมูลอยู่ถาวร"| X["💾"]
style E fill:#3f2d5c,stroke:#a855f7,color:#fff
style A fill:#14532d,stroke:#22c55e,color:#fff
ยิงตามลำดับนี้แล้วติ๊กทีละข้อ จดผลจริงที่ได้เทียบกับที่ควรได้
[ ] 1. GET /books -> 200, ได้ Array
[ ] 2. GET /books/1 -> 200, ได้ Object เล่มเดียว
[ ] 3. GET /books/999 -> 404
[ ] 4. GET /books/abc -> 404 (ไม่ใช่ 500!)
[ ] 5. POST /books {ครบทุกฟิลด์} -> 201, มี id ใหม่ไม่ซ้ำใคร
[ ] 6. POST /books {} -> 400
[ ] 7. POST /books {"title":""} -> 400
[ ] 8. PUT /books/1 {ครบทุกฟิลด์} -> 200, id ยังเป็น 1
[ ] 9. PUT /books/999 -> 404
[ ] 10. DELETE /books/1 -> 200
[ ] 11. DELETE /books/1 (ซ้ำ) -> 404
[ ] 12. GET /books -> จำนวนตรงกับที่คาด
[ ] 13. ปิด Server (Ctrl+C) แล้วเปิดใหม่
GET /books -> ข้อมูลยังอยู่ครบเหมือนเดิม
เคสที่ 13 คือหัวใจของ Chapter นี้ — เปิดไฟล์
data/books.jsonดูควบคู่ไปด้วย ทุกอย่างที่คุณเพิ่ม/แก้/ลบผ่าน API อยู่ในไฟล์นั้นจริงๆ ไม่ได้หายไปไหน
เคสที่ 4 สำคัญ — ยิง
/books/abcแล้วNumber("abc")จะได้NaNซึ่งNaN === NaNเป็นfalseเสมอ →.find()ไม่เจอ → ตอบ 404 พอดี ✅ ถ้าได้ 500 แปลว่ามีบางอย่างในโค้ดพังก่อนถึงจุดนั้น ต้องไปหา
สังเกตว่า 3 route มีโค้ดเหมือนกันเป๊ะๆ
const books = readBooks();
const id = Number(req.params.id);
const index = books.findIndex((b) => b.id === id);
if (index === -1) return res.status(404).json({ error: "ไม่พบหนังสือเล่มนี้" });
ดึงออกมาเป็นฟังก์ชันได้
function findBookIndex(req, res, books) {
const id = Number(req.params.id);
const index = books.findIndex((b) => b.id === id);
if (index === -1) {
res.status(404).json({ error: "ไม่พบหนังสือเล่มนี้" });
return null;
}
return index;
}
// ใช้งาน
app.delete("/books/:id", (req, res) => {
const books = readBooks();
const index = findBookIndex(req, res, books);
if (index === null) return; // ตอบ 404 ไปแล้วข้างใน
// ...
});
ทำไมต้องส่ง
booksเข้าไปด้วย? เพราะ route เจ้าของต้องใช้ Array ตัวเดียวกันนั้นต่อ เพื่อเอาไปwriteBooks()— ถ้าให้ฟังก์ชันไปreadBooks()เองข้างใน จะกลายเป็นคนละก้อน แล้วสิ่งที่เซฟลงไฟล์จะไม่ตรงกับที่ตรวจไว้
Chapter 5 จะสอนวิธีที่สวยกว่านี้ด้วย Middleware — ตอนนี้แค่รู้ว่าโค้ดซ้ำเป็นสัญญาณว่ามี วิธีที่ดีกว่ารออยู่
| อาการ | สาเหตุที่พบบ่อยที่สุด |
|---|---|
| ได้ 404 ทั้งที่ข้อมูลมี | ลืม Number(req.params.id) |
Cannot set headers after they are sent |
ลืม return หน้า res.status(404) |
req.body undefined |
ลืม app.use(express.json()) หรือวางใต้ route |
| แก้ตัวแรกแล้วได้ 404 | ใช้ if (!index) แทน if (index === -1) |
id หายหลังแก้ไข |
ลืม spread ...books[index] |
| ตอบสำเร็จแต่ข้อมูลไม่เปลี่ยน | ลืม writeBooks() — สาเหตุอันดับ 1 ของ Chapter นี้ |
| ลบแล้วไม่หาย | เอา books ตัวเดิมไปเซฟ แทนที่จะเซฟผลของ .filter() |
ENOENT ... data/books.json |
รัน Server ผิดโฟลเดอร์ — ต้อง cd เข้า books-api แล้ว npm run dev |
| Server รีสตาร์ทเองทุกครั้งที่ยิง POST | nodemon จับไฟล์ข้อมูลด้วย — ต้องมี nodemonConfig ที่ข้าม data/ |
| id ใหม่ซ้ำกับของเก่า | ใช้ books.length + 1 |
คุณทำได้แล้ว:
สิ่งที่ยังขาดและจะได้เรียนต่อ:

Boss Battle — Books API เวอร์ชันสมบูรณ์
รวม Mission 1-3 ให้เป็นไฟล์เดียวที่ทำงานครบ แล้วผ่าน Test Checklist ทั้ง 13 ข้อ ในบทเรียนด้านบน
GET /books/search?q=คำค้น — ค้นหาหนังสือจากชื่อ
title มีคำค้นอยู่ (ไม่สนตัวพิมพ์เล็กใหญ่)q มา → ตอบ 400[] (ไม่ใช่ 404!)Validation เพิ่ม — ถ้าส่ง year มาแล้วไม่ใช่ตัวเลข หรือเป็นปีในอนาคต
→ ตอบ 400
GET /stats — ตอบสถิติ
{
"total": 3,
"oldest": { "title": "สี่แผ่นดิน", "year": 1953 },
"newest": { "title": "แฮร์รี่ พอตเตอร์", "year": 1997 }
}
จด Test Checklist ทั้ง 13 ข้อว่าได้ Status อะไรจริงๆ พร้อมกาว่าตรงกับที่ควรได้หรือไม่

ข้อ 1. GET /books/abc (id ไม่ใช่ตัวเลข) ควรได้ Status อะไร
ข้อ 2. GET /books/search?q=xyz ที่ไม่เจออะไรเลย ควรตอบอะไร
ข้อ 3. ทำไมควรใช้ .sort() กับ [...books] แทนที่จะใช้ books ตรงๆ
ข้อ 4. route ไหนบ้างที่ต้องเรียก writeBooks()
ข้อ 5. การดึง validateBook() ออกมาเป็นฟังก์ชันแยกมีข้อดีอย่างไร
ข้อ 6. ถ้าจะเพิ่ม route GET /books/latest ต้องวางไว้ตรงไหน
ข้อ 7. ตอนนี้เก็บข้อมูลลงไฟล์ได้แล้ว แต่จุดอ่อนที่ใหญ่ที่สุดที่เหลืออยู่คืออะไร