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

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

เหลืออีก 2 ตัวก็ครบ CRUD แล้ว — แก้ไขกับลบ
ฝากระวังเรื่องเดียว: ตอนแก้ข้อมูล อย่าให้ id หายไปนะ เจอบั๊กนี้ในทีมมาหลายรอบแล้ว
แก้ชื่อหนังสือแล้ว id หายทั้งเล่ม กลายเป็นข้อมูลกำพร้า
เป้าหมายของ Mission นี้: ทำ U และ D ใน CRUD ให้ครบ — แก้ไขและลบข้อมูล
จบ Mission นี้คุณจะ
PUT /books/:id แก้ไขข้อมูลได้DELETE /books/:id ลบข้อมูลได้ข้อสังเกตสำคัญ: การแก้ไขต้องรู้ 2 อย่างพร้อมกัน
PUT /books/2
| |
| +--> "แก้เล่มไหน" มาจาก req.params (URL)
+---------> "แก้เป็นอะไร" มาจาก req.body (Body)
นี่ต่างจาก GET (ใช้แค่ params) และ POST (ใช้แค่ body) — PUT ใช้ทั้งคู่
PUT vs PATCH
| PUT | PATCH | |
|---|---|---|
| ความหมาย | แทนที่ทั้งชิ้น | แก้เฉพาะบางฟิลด์ |
ถ้าส่งมาแค่ title |
ตามหลักการ ฟิลด์อื่นควรถูกล้าง | ฟิลด์อื่นคงเดิม |
| ใช้ในคอร์สนี้ | ✅ ใช้ตัวนี้ | ไม่ใช้ |
ในทางปฏิบัติจริง ทีมส่วนใหญ่ใช้ PUT แบบ "ส่งอะไรมาก็แก้อันนั้น" (พฤติกรรมแบบ PATCH) ซึ่งเทคนิคแล้วไม่ตรงมาตรฐานเป๊ะ แต่ยอมรับกันทั่วไป — คอร์สนี้จะทำแบบนี้เพราะใช้งานได้จริงกว่า
flowchart TD
subgraph PUT["PUT /books/2"]
P1["อ่าน id จาก req.params"] --> P2["หา index ด้วย findIndex()"]
P2 --> P3{"เจอไหม?"}
P3 -->|"ไม่เจอ"| P4["404"]
P3 -->|"เจอ"| P5["รวมข้อมูลเก่า + ใหม่<br/>ด้วย spread operator"]
P5 --> P6["books[index] = merged"]
P6 --> P7["200 + ส่งข้อมูลใหม่กลับ"]
end
style P4 fill:#5c2d2d,stroke:#ef4444,color:#fff
style P7 fill:#14532d,stroke:#22c55e,color:#fff
app.put("/books/:id", (req, res) => {
const books = readBooks();
const id = Number(req.params.id); // (1)
const index = books.findIndex((b) => b.id === id); // (2)
if (index === -1) { // (3)
return res.status(404).json({ error: "ไม่พบหนังสือเล่มนี้" });
}
const { title, author, year } = req.body;
if (!title || !author) { // (4)
return res.status(400).json({ error: "ต้องระบุ title และ author" });
}
books[index] = { ...books[index], title, author, year }; // (5)
writeBooks(books); // (6)
res.json(books[index]); // (7)
});
(1) Number() อีกแล้ว — จำไว้ให้ขึ้นใจ
(2) .findIndex() ไม่ใช่ .find() — เพราะเราต้องรู้ ตำแหน่ง เพื่อเอาไปเขียนทับ
ไม่ใช่แค่ตัวข้อมูล
| คืนค่าอะไร | ไม่เจอคืนอะไร | |
|---|---|---|
.find() |
ตัวข้อมูล (Object) | undefined |
.findIndex() |
ตำแหน่ง (ตัวเลข) | -1 |
(3) เช็ก === -1 ไม่ใช่ !index — กับดักคลาสสิก! ถ้าข้อมูลอยู่ตำแหน่งแรก index จะเป็น
0 ซึ่งเป็น falsy → !index จะเป็น true → ตอบ 404 ทั้งที่เจอ
if (!index) // ❌ พังเมื่อ index = 0
if (index === -1) // ✅ ถูกต้อง
(4) Validate ด้วยเสมอ — PUT ก็รับข้อมูลจากผู้ใช้เหมือน POST
(5) Spread operator { ...books[index], title, author, year }
// ข้อมูลเดิม
{ id: 2, title: "เพชรพระอุมา", author: "พนมเทียน", year: 1964 }
// spread แล้วเขียนทับ 3 ฟิลด์
{ ...เดิม, title: "ใหม่", author: "ใหม่", year: 2020 }
// ผลลัพธ์ — id ยังอยู่! เพราะ spread ก๊อปมาให้แล้ว
{ id: 2, title: "ใหม่", author: "ใหม่", year: 2020 }
ทำไมไม่เขียน books[index] = { title, author, year } เฉยๆ? เพราะ id จะหายไป!
spread ช่วยรักษาฟิลด์ที่เราไม่ได้แตะไว้ (ใช้เทคนิคเดียวกับตอน setState ใน React)
(6) writeBooks(books) — เซฟลงไฟล์ ลืมบรรทัดนี้เมื่อไหร่ = แก้ไม่ติด
(ลอง curl ดูจะเห็นค่าใหม่ แต่พอ GET อีกทีกลับเป็นค่าเดิม)
(7) ตอบ 200 (ไม่ใช่ 201 เพราะไม่ได้สร้างของใหม่) พร้อมข้อมูลหลังแก้ไข
app.delete("/books/:id", (req, res) => {
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: "ไม่พบหนังสือเล่มนี้" });
}
const deleted = books[index]; // เก็บไว้ก่อนลบ เพื่อส่งกลับ
const remaining = books.filter((b) => b.id !== id);
writeBooks(remaining); // เซฟ Array ใหม่ทับไฟล์เดิม
res.json({ message: "ลบสำเร็จ", book: deleted });
});
.filter() สร้าง Array ใหม่ ที่มีเฉพาะสมาชิกที่ผ่านเงื่อนไข — ของเดิมไม่ถูกแตะ
เราจึงรับผลลัพธ์ใส่ตัวแปรใหม่ (remaining) แล้วเอาตัวนั้นเซฟลงไฟล์
ข้อดีของการเก็บลงไฟล์: เพราะเรา
readBooks()ใหม่ทุกครั้ง ตัวแปรbooksเลยเป็นแค่ ของชั่วคราวใน route นั้น — ประกาศconstได้หมดทุกที่ ไม่ต้องมีletระดับบนสุดของไฟล์ ให้กังวลว่าใครไปแก้ตรงไหน
// อีกวิธีที่ใช้ได้ — splice แก้ Array เดิมโดยตรง แล้วเซฟ books
books.splice(index, 1);
writeBooks(books);
ทั้งสองแบบใช้ได้ แต่ .filter() อ่านง่ายกว่าและเป็นสไตล์ที่นิยมกว่า
DELETE สำเร็จควรตอบอะไร? เลือกได้ 2 แบบ
200 + ข้อมูลที่ลบ (แบบในโค้ดนี้ — ดีตอน debug เพราะเห็นว่าลบอะไรไป)204 No Content + ไม่มี body (res.status(204).send())| ปัญหา | สาเหตุ | วิธีแก้ |
|---|---|---|
| แก้ข้อมูลตัวแรก (index 0) แล้วได้ 404 | เช็ก if (!index) ซึ่ง 0 เป็น falsy |
ใช้ if (index === -1) |
แก้แล้ว id หายไปจากข้อมูล |
เขียนทับทั้ง Object โดยไม่ spread | ใช้ { ...books[index], ...ค่าใหม่ } |
| ลบ/แก้แล้วตอบ 200 แต่ข้อมูลไม่เปลี่ยน | ลืม writeBooks(...) |
ทุก route ที่ POST/PUT/DELETE ต้องจบด้วย writeBooks() เสมอ |
| ลบแล้วข้อมูลไม่หาย | ใช้ .filter() แต่เอา books ตัวเดิมไปเซฟ |
ต้องเซฟผลลัพธ์ของ filter — writeBooks(remaining) |
| ยิง DELETE แล้ว Server รีสตาร์ทเอง | nodemon เห็นไฟล์ data/books.json เปลี่ยน |
เช็กว่ามี nodemonConfig ที่ข้าม data/ ใน package.json แล้ว (ตั้งไว้ตั้งแต่ Chapter 2 Boss) |
PUT แล้ว year กลายเป็น undefined |
ผู้ใช้ไม่ได้ส่ง year มา |
ใช้ year: year ?? books[index].year เพื่อคงค่าเดิม |
| ลบซ้ำ 2 ครั้งแล้วครั้งที่ 2 ยังตอบ 200 | ไม่ได้เช็กก่อนลบ | เช็ก index === -1 ก่อนเสมอ |
req.params (แก้เล่มไหน) และ req.body (แก้เป็นอะไร).findIndex() คืนตำแหน่ง ไม่เจอได้ -1 → ต้องเช็ก === -1 ไม่ใช่ !indexid) หายไป.filter() ได้ Array ใหม่ แล้วเอาตัวใหม่ไป writeBooks()writeBooks() เสมอ ไม่งั้นไฟล์ไม่เปลี่ยน404 เสมอ
Workshop — ทำ CRUD ให้ครบวงจร
PUT /books/:id — แก้ไขหนังสือ
title หรือ author → 400id ต้องยังอยู่)DELETE /books/:id — ลบหนังสือ
ทำตามลำดับแล้วจดผลทุกขั้น
| ขั้น | Request | คาดว่าจะได้ |
|---|---|---|
| 1 | GET /books |
เห็นกี่เล่ม? |
| 2 | PUT /books/1 ส่ง {"title":"HP ฉบับแก้ไข","author":"J.K.R."} |
200 — id ยังเป็น 1 ไหม? |
| 3 | GET /books/1 |
เห็นข้อมูลใหม่ไหม? |
| 4 | DELETE /books/2 |
200 |
| 5 | GET /books |
เหลือกี่เล่ม? |
| 6 | DELETE /books/2 (ซ้ำอีกครั้ง) |
ควรได้ 404 |
| 7 | PUT /books/999 |
ควรได้ 404 |
ลองเปลี่ยนเงื่อนไขใน PUT จาก if (index === -1) เป็น if (!index)
แล้วยิง PUT /books/1 (เล่มแรกสุดของ Array)
ปิด Server ด้วย Ctrl + C แล้วเปิดใหม่ จากนั้นยิง GET /books
data/books.json เทียบกับผลที่ API ตอบ — ตรงกันไหม?
ข้อ 1. PUT ต่างจาก POST อย่างไรในแง่ของข้อมูลที่ใช้
ข้อ 2. .findIndex() คืนค่าอะไรเมื่อหาไม่เจอ
ข้อ 3. ทำไมต้องใช้ spread operator ตอนแก้ไขข้อมูล
ข้อ 4. ใน DELETE ทำไมต้อง writeBooks(remaining) ไม่ใช่ writeBooks(books)
ข้อ 5. DELETE ข้อมูลที่ถูกลบไปแล้วซ้ำอีกครั้ง ควรตอบอะไร
ข้อ 6. ทำไมข้อมูลถึงไม่หายเมื่อปิด Server แล้วเปิดใหม่