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

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

อ่านได้แล้ว ทีนี้ต้องเพิ่มได้ด้วย
ขอเน้นเรื่องเดียว — อย่าเชื่อข้อมูลที่ส่งเข้ามา ถ้าเขาไม่ส่ง title มาแล้วเรา บันทึกลงไปดื้อๆ ระบบจะมีขยะเต็มไปหมด ตรวจให้ดีก่อนรับนะ
เป้าหมายของ Mission นี้: ทำ C ใน CRUD — รับข้อมูลจาก Body แล้วเพิ่มเข้าระบบ พร้อม ตรวจสอบความถูกต้อง
จบ Mission นี้คุณจะ
req.body ได้express.json() และเกิดอะไรขึ้นถ้าไม่มีจำ Chapter 1 ได้ไหมว่า Request มี 4 ส่วน? Body คือส่วนที่ 4 — ก้อนข้อมูลที่ส่งมาพร้อม POST/PUT
POST /books HTTP/1.1
Content-Type: application/json <-- Header บอกว่า Body เป็น JSON
{"title":"1984","author":"George Orwell","year":1949} <-- Body
ปัญหา: Body ที่วิ่งมาตามสายเป็นแค่ ข้อความยาวๆ (จริงๆ คือ stream ของ byte) Express ไม่ได้แปลงให้เองโดยอัตโนมัติ
app.post("/books", (req, res) => {
console.log(req.body); // undefined ❌ ถ้ายังไม่ได้เปิด express.json()
});
วิธีแก้ — เพิ่มบรรทัดเดียว ไว้เหนือ route ทั้งหมด
app.use(express.json());
บรรทัดนี้บอก Express ว่า "ทุก Request ที่เข้ามา ถ้า Header บอกว่าเป็น JSON ให้แกะข้อความ
กลับเป็น Object แล้วเสียบไว้ที่ req.body ให้ด้วย"
เจ้าสิ่งนี้เรียกว่า Middleware — Chapter 5 จะเจาะลึกว่ามันทำงานยังไง ตอนนี้แค่จำว่า ถ้าจะรับ Body ต้องมีบรรทัดนี้เสมอ
flowchart TD
A["POST /books<br/>Body: {title, author, year}"] --> B["express.json()<br/>แกะ JSON ใส่ req.body"]
B --> C["readBooks()"]
C --> D{"มี title ไหม?"}
D -->|"ไม่มี"| E["400 Bad Request"]
D -->|"มี"| F["สร้าง id ใหม่"]
F --> G["books.push(newBook)"]
G --> H["writeBooks(books)<br/>เซฟลงไฟล์"]
H --> I["201 Created<br/>ส่ง newBook กลับ"]
style B fill:#3f2d5c,stroke:#a855f7,color:#fff
style C fill:#3f2d5c,stroke:#a855f7,color:#fff
style H fill:#3f2d5c,stroke:#a855f7,color:#fff
style E fill:#5c2d2d,stroke:#ef4444,color:#fff
style I fill:#14532d,stroke:#22c55e,color:#fff
const app = express();
app.use(express.json()); // ⚠️ ต้องอยู่ "เหนือ" route ทุกเส้น
ทำไมต้องอยู่บน? เพราะ Express ทำงานจากบนลงล่าง — ถ้าวางไว้ใต้ route ตอน route ทำงาน
ยังไม่มีใครแกะ Body ให้ req.body ก็ยังเป็น undefined อยู่ดี
app.post("/books", (req, res) => {
console.log(req.body); // ดูก่อนว่าได้อะไรมา
res.status(201).json(req.body);
});
ยิงทดสอบด้วย Thunder Client: Method POST, URL http://localhost:3000/books,
แท็บ Body → JSON → ใส่
{ "title": "1984", "author": "George Orwell", "year": 1949 }
ดูที่ terminal จะเห็น Object พิมพ์ออกมา — แปลว่า express.json() ทำงานแล้ว
อย่าเชื่อข้อมูลที่ Client ส่งมาเด็ดขาด (กฎทองจาก Chapter 1) ผู้ใช้อาจส่งมาไม่ครบ ส่งมาผิดชนิด หรือส่งมั่วๆ
app.post("/books", (req, res) => {
const { title, author, year } = req.body; // (1)
if (!title || !author) { // (2)
return res.status(400).json({ error: "ต้องระบุ title และ author" });
}
const books = readBooks(); // (3)
const newBook = { // (4)
id: books.length > 0 ? Math.max(...books.map((b) => b.id)) + 1 : 1,
title,
author,
year: year ?? null,
};
books.push(newBook); // (5)
writeBooks(books); // (6)
res.status(201).json(newBook); // (7)
});
สังเกตลำดับ: เรา validate ก่อน แล้วค่อย
readBooks()— ถ้าข้อมูลไม่ผ่านก็ไม่ต้อง เสียเวลาเปิดไฟล์เลย
(1) Destructuring — ดึงค่าออกจาก Object แบบเดียวกับที่ใช้ใน React (const { data } = props)
ถ้าไม่มี key นั้น จะได้ undefined
(2) Validation — !title เป็นจริงเมื่อ title เป็น undefined, "", หรือ null
ทั้งหมดนี้คือกรณีที่เราไม่ต้องการ → ตอบ 400 (Client ผิด ไม่ใช่เราผิด) และอย่าลืม return
(3) readBooks() — ดึงข้อมูลล่าสุดจากไฟล์ขึ้นมาก่อน จะได้รู้ว่าตอนนี้มี id อะไรไปแล้วบ้าง
(4) การสร้าง id — ทำไมไม่ใช้ books.length + 1?
// ❌ อันตราย
id: books.length + 1
สมมติมี 3 เล่ม id = 1, 2, 3 → ลบเล่ม id 2 ทิ้ง → เหลือ 2 เล่ม (length = 2) →
เพิ่มเล่มใหม่ได้ id = 3 → ซ้ำกับเล่มเดิมที่มี id 3 อยู่แล้ว!
// ✅ ปลอดภัย — หา id ที่มากที่สุดแล้ว +1
id: books.length > 0 ? Math.max(...books.map((b) => b.id)) + 1 : 1
Math.max(...) ต้องเช็ก Array ว่างก่อน เพราะ Math.max() ของ Array ว่างจะได้ -Infinity
ข่าวดี: คอร์สหน้า PostgreSQL จะจัดการเรื่อง id ให้ทั้งหมดด้วย
SERIAL— เราไม่ต้องคิดเองอีกเลย
(5) books.push() เพิ่มเข้า Array ที่อยู่ในหน่วยความจำ — ตอนนี้ไฟล์ยังไม่เปลี่ยน
(6) writeBooks(books) — บรรทัดที่ลืมกันมากที่สุด
ถ้าลืมบรรทัดนี้ API จะตอบ 201 สำเร็จสวยงาม แต่พอยิง GET /books กลับไม่เห็นเล่มใหม่
เพราะ push() แก้แค่ตัวแปรในหน่วยความจำที่หายไปเมื่อ request จบ ต้องเซฟลงไฟล์เสมอ
เปิดไฟล์
data/books.jsonค้างไว้ข้างๆ ตอนยิง POST — จะเห็นเล่มใหม่โผล่ในไฟล์ทันที ที่กด send นี่คือภาพที่ช่วยให้เข้าใจว่า "เซฟข้อมูล" หมายความว่าอะไรจริงๆ
(7) 201 Created ไม่ใช่ 200 — และควรส่งข้อมูลที่สร้างเสร็จกลับไปด้วย เพราะ Frontend
ต้องการ id ที่เพิ่งเกิดขึ้น (Frontend ไม่มีทางรู้เองว่าได้ id อะไร)
| ปัญหา | สาเหตุ | วิธี Debug |
|---|---|---|
req.body เป็น undefined |
ลืม app.use(express.json()) |
เพิ่มบรรทัดนี้เหนือ route ทั้งหมด |
req.body เป็น {} ว่างเปล่า |
ไม่ได้ตั้ง Header Content-Type: application/json ตอนยิง |
ใน Thunder Client เลือก Body เป็น JSON ไม่ใช่ Text |
Unexpected token } in JSON |
JSON ที่พิมพ์ใน Body ผิดรูปแบบ (คอมมาเกิน / single quote) | อ่าน JSON ที่ส่งอีกรอบ ให้ทุก key อยู่ใน "..." |
ตอบ 201 แล้ว แต่ GET /books ไม่เห็นเล่มใหม่ |
ลืม writeBooks(books) |
push() แก้แค่ตัวแปรในหน่วยความจำ ต้องเซฟลงไฟล์ด้วยเสมอ |
| ยิง POST แล้ว Server รีสตาร์ทเองทุกครั้ง | nodemon เห็นว่า data/books.json เปลี่ยน เลยรีสตาร์ทให้ |
ใส่ nodemonConfig ให้ข้าม data/ (ตั้งไว้แล้วตั้งแต่ Chapter 2 Boss) |
| เพิ่มข้อมูลแล้ว id ซ้ำ | ใช้ books.length + 1 |
เปลี่ยนเป็น Math.max() |
| ตอบ 200 แทน 201 | ลืมใส่ .status(201) |
POST ที่สร้างของใหม่สำเร็จควรเป็น 201 |
TypeError: Cannot destructure property |
req.body เป็น undefined อยู่ |
สาเหตุเดียวกับข้อแรก |
ผู้ใช้ส่ง {"title": ""} แล้วผ่าน |
เช็กแค่ title === undefined |
ใช้ !title ครอบคลุมทั้ง undefined, null, และ string ว่าง |
req.bodyapp.use(express.json()) เหนือ route ทั้งหมด ไม่งั้น req.body เป็น undefined400 (Client ผิด) พร้อม returnMath.max() ไม่ใช่ length + 1readBooks() → แก้ Array → writeBooks()push() อย่างเดียวไม่พอ — ไม่เรียก writeBooks() = ไฟล์ไม่เปลี่ยน201 และส่งข้อมูลที่สร้างกลับไปด้วย
Workshop — เพิ่มหนังสือใหม่ผ่าน POST
app.use(express.json()); เหนือ route ทั้งหมดPOST /books ที่
title, author, year จาก req.bodytitle หรือไม่มี author → ตอบ 400 พร้อมข้อความบอกว่าขาดอะไรid ใหม่ด้วย Math.max() (ห้ามใช้ length + 1)push เข้า Array แล้ว writeBooks() เซฟลงไฟล์ จากนั้นตอบ 201
พร้อมข้อมูลเล่มที่เพิ่งสร้างเปิด
data/books.jsonค้างไว้ครึ่งจอ ตอนยิง POST จะได้เห็นเล่มใหม่โผล่ในไฟล์ ทันทีที่กด send
ยิงด้วย Thunder Client แล้วจด Status Code ที่ได้
| # | Body ที่ส่ง | ควรได้ Status |
|---|---|---|
| 1 | {"title":"1984","author":"Orwell","year":1949} |
? |
| 2 | {"title":"1984"} (ไม่มี author) |
? |
| 3 | {} (ว่างเปล่า) |
? |
| 4 | {"title":"","author":"Orwell"} (title เป็นค่าว่าง) |
? |
ยิงเสร็จแล้วเรียก GET /books เช็กว่าข้อมูลเข้าไปจริงไหม แล้วเปิดไฟล์
data/books.json ดูด้วย ว่ามีเล่มใหม่อยู่ในนั้นจริง
ปิด app.use(express.json()) (ใส่ // หน้าไว้) แล้วยิง POST ใหม่
req.body ได้ค่าอะไร? (ใช้ console.log ดูใน terminal)
ข้อ 1. app.use(express.json()) ทำอะไร
ข้อ 2. ผู้ใช้ส่ง POST มาโดยไม่มี title ควรตอบ Status อะไร
ข้อ 3. ทำไมไม่ควรสร้าง id ด้วย books.length + 1
ข้อ 4. POST สร้างข้อมูลสำเร็จ ควรส่งอะไรกลับไปให้ Client
ข้อ 5. if (!title) ครอบคลุมกรณีใดบ้าง
ข้อ 6. เขียน books.push(newBook) แล้วตอบ 201 ไป แต่ยิง GET /books กลับไม่เห็นเล่มใหม่ เพราะอะไร
ข้อ 7. ยิง POST แล้ว req.body ได้ {} ว่างเปล่า สาเหตุที่เป็นไปได้มากที่สุดคืออะไร