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

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

เมื่อกี้ route ตอบข้อความปลอมหมดเลย วันนี้ใส่ข้อมูลจริงเข้าไปได้แล้ว
เริ่มจากตัว R ของ CRUD ก่อน — อ่านทั้งหมด กับอ่านรายเล่ม อย่าลืมเคส "หาไม่เจอ" ด้วยนะ ลูกค้าจะยิง id มั่วๆ มาแน่นอน
เป้าหมายของ Mission นี้: ทำ R ใน CRUD — อ่านข้อมูลทั้งหมดและอ่านรายตัว พร้อมจัดการ กรณีหาไม่เจอ
จบ Mission นี้คุณจะ
data/books.json แล้วอ่านขึ้นมาใช้ด้วย fs ได้GET /books คืนข้อมูลทั้งหมดได้GET /books/:id คืนข้อมูลรายตัวได้CRUD คือชื่อย่อของ 4 อย่างที่ทุกระบบข้อมูลในโลกต้องทำได้
+--------+-------------+--------+------------------+
| ตัวอักษร | คำเต็ม | Method | ตัวอย่างในชีวิตจริง |
+--------+-------------+--------+------------------+
| C | Create | POST | เพิ่มโพสต์ใหม่ |
| R | Read | GET | เลื่อนดูฟีด |
| U | Update | PUT | แก้ไขโพสต์ |
| D | Delete | DELETE | ลบโพสต์ |
+--------+-------------+--------+------------------+
ไม่ว่าจะเป็น Facebook, Shopee, หรือระบบบัญชี — เบื้องหลังคือ CRUD ทั้งหมด
flowchart TD
A["GET /books/2"] --> B["Express จับคู่ route<br/>app.get('/books/:id')"]
B --> C["readBooks()<br/>อ่าน data/books.json"]
C --> D["อ่าน id จาก req.params<br/>ได้ '2' (string!)"]
D --> E["ค้นด้วย .find()"]
E --> F{"เจอไหม?"}
F -->|"เจอ"| G["res.json(book)<br/>Status 200"]
F -->|"ไม่เจอ"| H["res.status(404)<br/>+ ข้อความ error"]
style C fill:#3f2d5c,stroke:#a855f7,color:#fff
style G fill:#14532d,stroke:#22c55e,color:#fff
style H fill:#5c2d2d,stroke:#ef4444,color:#fff
สร้างโฟลเดอร์ data แล้วสร้างไฟล์ data/books.json ข้างใน ใส่ข้อมูลตั้งต้น 3 เล่ม
[
{ "id": 1, "title": "แฮร์รี่ พอตเตอร์", "author": "J.K. Rowling", "year": 1997 },
{ "id": 2, "title": "เพชรพระอุมา", "author": "พนมเทียน", "year": 1964 },
{ "id": 3, "title": "สี่แผ่นดิน", "author": "ม.ร.ว.คึกฤทธิ์ ปราโมช", "year": 1953 }
]
ระวัง — JSON ไม่ใช่ JavaScript ในไฟล์
.jsonชื่อ key ต้องมี"ครอบทุกตัว และ ห้ามมี,ก่อนวงเล็บปิด (ต่างจาก object ใน JS ที่ทำได้ทั้งคู่) ถ้าพิมพ์ผิดจะเจอ errorUnexpected tokenตอนรัน — VS Code จะขีดเส้นแดงให้เห็นก่อนแล้ว
ทำไมเก็บลงไฟล์ ไม่เก็บใน Array ในโค้ด? เพราะ 2 เหตุผล
Node.js มี module ชื่อ fs (File System) ติดมาให้ในตัว — ไม่ต้อง npm install
นี่คือของที่เบราว์เซอร์ทำไม่ได้ และเป็นเหตุผลหนึ่งที่เราต้องมี Backend
เพิ่มไว้บนสุดของ index.js (บรรทัดเดียวกับ import อื่นๆ) แล้วตามด้วยฟังก์ชัน 2 ตัว
import fs from "node:fs";
const DATA_FILE = "./data/books.json";
// อ่านไฟล์ -> ได้ Array ของ object กลับมาใช้ได้เลย
function readBooks() {
const text = fs.readFileSync(DATA_FILE, "utf-8");
return JSON.parse(text);
}
// เขียน Array กลับลงไฟล์
function writeBooks(books) {
fs.writeFileSync(DATA_FILE, JSON.stringify(books, null, 2), "utf-8");
}
อธิบายทีละจุด
| โค้ด | ทำอะไร |
|---|---|
fs.readFileSync(...) |
อ่านไฟล์ออกมาเป็น ข้อความ (ยังใช้เป็น object ไม่ได้) |
JSON.parse(text) |
แปลงข้อความ JSON → Array/Object ของ JavaScript |
JSON.stringify(books) |
แปลงกลับ Array/Object → ข้อความ JSON |
null, 2 |
สั่งให้จัดย่อหน้าสวยๆ 2 space — สำคัญมาก ถ้าไม่ใส่ ไฟล์จะเป็นบรรทัดเดียวยาวเหยียด อ่านไม่ออก |
"utf-8" |
บอกว่าเป็นข้อความ ไม่ใช่ไฟล์รูป — ถ้าไม่ใส่จะได้ข้อมูลดิบที่อ่านไม่รู้เรื่อง (และภาษาไทยจะเพี้ยน) |
ทำไมใช้
readFileSyncที่มีคำว่า Sync? เพราะมันอ่านให้เสร็จก่อนแล้วค่อยไปบรรทัดถัดไป ทำให้เขียนโค้ดตรงไปตรงมา ไม่ต้องมีasync/awaitมากวนตอนนี้ — ของจริงในระบบใหญ่จะใช้แบบ async ซึ่งเราจะได้เจอตอนต่อ PostgreSQL ในคอร์สหน้า
app.get("/books", (req, res) => {
const books = readBooks();
res.json(books);
});
res.json() แปลง Array เป็น JSON ให้เอง
สังเกต: เราเรียก
readBooks()ข้างในทุก route ไม่ใช่อ่านครั้งเดียวไว้ข้างนอก เพราะอยากได้ข้อมูลล่าสุดจากไฟล์เสมอ — ถ้าเราไปแก้books.jsonด้วยมือ ก็เห็นผลทันที โดยไม่ต้องรีสตาร์ท Server
คำถามชวนคิด: ถ้า
booksเป็น Array ว่าง[]ควรตอบ 404 ไหม? ไม่ — ให้ตอบ200พร้อม[]เพราะ "ไม่มีข้อมูล" ≠ "หาไม่เจอ" ตัว collection ยังอยู่ดี
app.get("/books/:id", (req, res) => {
const books = readBooks();
const id = Number(req.params.id); // (1)
const book = books.find((b) => b.id === id); // (2)
if (!book) { // (3)
return res.status(404).json({ error: "ไม่พบหนังสือเล่มนี้" });
}
res.json(book); // (4)
});
อธิบายทีละบรรทัด
(1) Number(req.params.id) — บรรทัดที่พลาดกันเยอะที่สุดในคอร์ส
ค่าที่มาจาก URL เป็น string เสมอ เพราะ URL คือข้อความ
// ยิง GET /books/2
req.params.id // "2" <-- string ไม่ใช่ number!
req.params.id === 2 // false ❌
Number(req.params.id) === 2 // true ✅
ถ้าลืมแปลง .find() จะไม่มีวันเจออะไรเลย และได้ 404 ทั้งที่ข้อมูลมีอยู่ — เป็นบั๊กที่ไล่หายาก
เพราะโค้ด "ดูเหมือนถูก"
(2) .find() คืนสมาชิกตัวแรกที่ตรงเงื่อนไข ถ้าไม่เจอคืน undefined
(3) return หน้า res.status(404) — คำว่า return ตรงนี้สำคัญมาก
// ❌ ผิด — ลืม return
if (!book) {
res.status(404).json({ error: "ไม่พบ" });
}
res.json(book); // บรรทัดนี้จะรันต่อ! -> Error: Cannot set headers after they are sent
res.json() ไม่ได้หยุดการทำงานของฟังก์ชัน มันแค่ "ส่งคำตอบ" — โค้ดบรรทัดถัดไปยังรันต่อ
แล้วพยายามส่งคำตอบซ้ำอีกรอบ ซึ่งทำไม่ได้ ต้องใส่ return ทุกครั้งที่ตอบกลับกลางทาง
(4) ถ้าผ่านมาถึงตรงนี้แปลว่าเจอแล้ว ส่งกลับได้เลย (Express ใส่ Status 200 ให้อัตโนมัติ)
curl http://localhost:3000/books # ได้ Array 3 เล่ม
curl http://localhost:3000/books/2 # ได้เพชรพระอุมา
curl http://localhost:3000/books/999 # ได้ 404 + ข้อความ error
ดูใน Thunder Client จะเห็น Status Code ชัดกว่า (200 เขียว / 404 แดง)
ลองพิสูจน์ว่ามันอ่านจากไฟล์จริง — เปิด data/books.json แก้ชื่อหนังสือเล่มที่ 2 แล้ว
save จากนั้นยิง curl http://localhost:3000/books/2 อีกครั้งโดยไม่ต้องรีสตาร์ท Server
→ ต้องได้ชื่อใหม่ทันที นี่คือหลักฐานว่าข้อมูลไม่ได้อยู่ในโค้ดแล้ว แต่อยู่ในไฟล์
| ปัญหา | สาเหตุ | วิธี Debug |
|---|---|---|
ยิง /books/1 แล้วได้ 404 ทั้งที่มีข้อมูล |
ลืม Number() → เทียบ "1" === 1 เป็น false |
console.log(typeof req.params.id) จะเห็นว่าเป็น string |
Cannot set headers after they are sent |
ลืม return หน้า res.status(404) |
ไล่ดูทุก res. ในฟังก์ชันว่ามีโอกาสรัน 2 ครั้งไหม |
books is not defined |
ลืมเรียก const books = readBooks(); ข้างใน route นั้น |
เพิ่มบรรทัดแรกของ route ให้ครบทุกเส้น |
/books/popular ได้ผลแปลกๆ |
ลำดับ route ผิด (Chapter 2) | ย้าย route เจาะจงขึ้นก่อน /:id |
ENOENT: no such file or directory, open './data/books.json' |
เปิด Server ผิดโฟลเดอร์ — ./ นับจากที่ที่รันคำสั่ง ไม่ใช่ที่ที่ไฟล์โค้ดอยู่ |
cd เข้า books-api ก่อน แล้วสั่ง npm start (อีกเหตุผลที่ควรใช้ npm start ไม่ใช่พิมพ์ path เอง) และเช็กว่าสะกด data/books.json ถูก |
Unexpected token } in JSON at position ... |
ไฟล์ books.json พิมพ์ผิด — key ไม่มี " ครอบ หรือมี , เกินก่อนวงเล็บปิด |
เปิดไฟล์ดูเส้นแดงที่ VS Code ขีดไว้ |
ไฟล์ books.json กลายเป็นบรรทัดเดียวยาวเหยียด |
ลืมใส่ null, 2 ใน JSON.stringify() |
เติมเข้าไป แล้วยิง request ใหม่ 1 ครั้ง ไฟล์จะจัดรูปให้เอง |
data/books.json — อ่านด้วย readBooks() เขียนด้วย writeBooks()fs เป็น module ที่ Node มีให้ในตัว (เบราว์เซอร์ทำแบบนี้ไม่ได้)JSON.parse() = ข้อความ → object / JSON.stringify(x, null, 2) = object → ข้อความจัดรูปสวยres.json(array) คืนข้อมูลทั้งหมดได้ทันทีreq.params.id เป็น string เสมอ → ต้อง Number() ก่อนเทียบกับ id ที่เป็นตัวเลข404 และต้อง return เสมอ200 พร้อม []
Workshop — ทำ Read ให้ครบ
data และไฟล์ data/books.json ใส่หนังสืออย่างน้อย 3 เล่ม
(แต่ละเล่มมี id, title, author, year)readBooks() และ writeBooks() ด้วย fsGET /books → คืน Array ทั้งหมดGET /books/:id → คืนเล่มเดียว, ถ้าไม่เจอตอบ 404 พร้อม { "error": "..." }data/books.json แล้วยิง GET /books
ใหม่โดยไม่รีสตาร์ท Server ต้องเห็นชื่อใหม่ทันทีเพิ่ม route GET /books/count ที่ตอบ { "total": 3 } (จำนวนหนังสือทั้งหมด)
ระวัง: ถ้าวางผิดที่ route นี้จะไม่ทำงาน — คิดให้ออกว่าต้องวางตรงไหน แล้วอธิบาย เหตุผลได้ด้วย
ลองลบ Number() ออก จาก GET /books/:id แล้วยิง /books/1 ดู
console.log(typeof req.params.id) แล้วดูใน terminal ว่าพิมพ์อะไรออกมาNumber() กลับเข้าไปนี่คือบั๊กที่คุณจะเจออีกหลายรอบในคอร์สนี้ ให้เจอตั้งแต่ตอนนี้จะได้จำได้

ข้อ 1. req.params.id มีชนิดข้อมูลเป็นอะไร
ข้อ 2. ทำไมต้องเขียน return หน้า res.status(404).json(...)
ข้อ 3. GET /books ที่ไม่มีหนังสือเลย ควรตอบอะไร
ข้อ 4. .find() คืนค่าอะไรเมื่อหาไม่เจอ
ข้อ 5. ทำไม route GET /books/count ต้องอยู่เหนือ GET /books/:id
ข้อ 6. JSON.parse() กับ JSON.stringify() ต่างกันยังไง
ข้อ 7. ทำไมเราเรียก readBooks() ไว้ข้างในแต่ละ route แทนที่จะอ่านไฟล์ครั้งเดียวไว้ข้างนอก