การสร้าง JSON API
การอ่าน request body
หัวข้อที่มีชื่อว่า “การอ่าน request body”ต่างจาก URL และ headers ที่มีพร้อมทันที body ของ request มาเป็น stream ของ chunk คุณต้องเก็บรวบรวมด้วย event data และ end:
function readBody(req) { return new Promise(function(resolve, reject) { let raw = ''; req.on('data', function(chunk) { raw += chunk; }); req.on('end', function() { resolve(raw); }); req.on('error', reject); });}เมื่อได้ string ดิบแล้ว ให้ parse ด้วย JSON.parse() ใน try/catch เพื่อจัดการ input ที่รูปแบบผิดพลาดอย่างสง่างาม
JSON API ขนาดเล็กที่เก็บข้อมูลใน memory
หัวข้อที่มีชื่อว่า “JSON API ขนาดเล็กที่เก็บข้อมูลใน memory”server ด้านล่างจัดการ list ของ item ใน memory โดยรองรับ:
GET /items— แสดง item ทั้งหมดPOST /items— เพิ่ม item (อ่าน JSON body)GET /items/:id— ดึง item เดียว (คืน 404 ถ้าไม่มี)
Needs the Node.js runtime — open in StackBlitz to run.
เปิดใน StackBlitz แล้วทดสอบ API เพิ่มเติมด้วย built-in HTTP client หรือ curl — เปลี่ยน 3000 เป็น port ที่พิมพ์ใน console:
curl http://localhost:3000/itemscurl -X POST http://localhost:3000/items -H 'Content-Type: application/json' -d '{"name":"TypeScript"}'curl http://localhost:3000/items/3curl http://localhost:3000/items/99สรุป status codes สำหรับ REST API
หัวข้อที่มีชื่อว่า “สรุป status codes สำหรับ REST API”| สถานการณ์ | Status Code |
|---|---|
| อ่านข้อมูลสำเร็จ | 200 OK |
| สร้าง resource สำเร็จ | 201 Created |
| field หายหรือ body ผิดรูปแบบ | 400 Bad Request |
| ไม่พบ resource | 404 Not Found |
| server error | 500 Internal Server Error |
Body size limit และ req.destroy()
หัวข้อที่มีชื่อว่า “Body size limit และ req.destroy()”helper readBody ด้านบนเก็บ chunk โดยไม่มี limit เลย — client สามารถ stream ข้อมูลหลาย gigabyte เข้า handler ที่แค่ concatenate ไปเรื่อย ๆ ทำให้ memory โตขึ้นจนกระบวนการล่ม body reader จริง ๆ ต้องติดตามขนาดที่รับมาสะสม แล้วหยุดทันทีเมื่อเกิน limit โดยเรียก req.destroy() เพื่อหยุดอ่าน body ที่เกินขนาดที่เหลือ และตอบ 413 Payload Too Large:
Output (verified on Node 24)
413 413 Payload Too Largeresponse ถูกเขียนไป ก่อน req.destroy() — ถ้าสลับลำดับกันจะทำให้ socket ถูกทำลายก่อนที่ body ของ 413 จะไปถึง client เลย ถ้าไม่มี limit เลย handler นี้จะเป็นช่องทาง denial-of-service แบบคลาสสิก: การ upload ที่ช้าและเกินขนาดเพียงครั้งเดียวสามารถ pin memory ไว้ไม่มีกำหนดในขณะที่ไม่มีอะไรอ่านมันจบสักที
JSON ที่ผิดรูปแบบกลายเป็น 400 ไม่ใช่ crash
หัวข้อที่มีชื่อว่า “JSON ที่ผิดรูปแบบกลายเป็น 400 ไม่ใช่ crash”JSON.parse() throw SyntaxError กับอะไรก็ตามที่ไม่ใช่ JSON ที่ถูกต้อง demo readBody/jsonApiCode ด้านบนห่อมันด้วย try อยู่แล้ว แต่คุ้มค่าที่จะดูมันแยกเดี่ยว ๆ เพราะการลืม try/catch นี้เป็นช่องโหว่ที่พบบ่อยที่สุดอันหนึ่งในโค้ด parse body ที่ AI เขียน:
Output (verified on Node 24)
400 { error: 'malformed JSON body' }ถ้าไม่มี try/catch การ throw ของ SyntaxError ข้างใน request listener ที่เป็น async จะ reject Promise ที่ listener นั้นคืนค่า — Node.js ไม่แปลงมันเป็น response ให้อัตโนมัติ request เลยค้างอยู่แค่นั้น และ rejection ก็กลายเป็น unhandled rejection
Byte length เทียบกับ string length สำหรับ body ที่ไม่ใช่ ASCII
หัวข้อที่มีชื่อว่า “Byte length เทียบกับ string length สำหรับ body ที่ไม่ใช่ ASCII”output ของ JSON.stringify เป็น JavaScript string และ .length นับ UTF-16 code unit — สำหรับ JSON ที่เป็น ASCII ล้วนตัวเลขนี้เท่ากับจำนวน byte พอดี แต่ทันทีที่ value มีข้อความไทย (หรือ emoji หรือภาษาที่ไม่ใช่ Latin ส่วนใหญ่) มันจะไม่เท่ากันอีกต่อไป:
const text = 'สวัสดี'; // Thai greeting - each character is multiple UTF-8 bytesconsole.log('.length (UTF-16 code units):', text.length);console.log('Buffer.byteLength (actual UTF-8 bytes):', Buffer.byteLength(text));res.end(data) คำนวณ Content-Length ให้คุณถูกต้องด้วยจำนวน byte จริง ดังนั้นเรื่องนี้แทบไม่กัดในโค้ดที่ปล่อยให้ res.end() ทำงานเอง มันจะกัดทันทีที่มีใครคำนวณความยาวสำหรับ header เอง, ตั้ง Content-Length ล่วงหน้าก่อนการ write() แบบ stream หรือทำ budget ขนาด body เอง — ให้ใช้ Buffer.byteLength(str) เสมอในกรณีเหล่านั้น ไม่ใช่ str.length
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”handler ของ JSON API เป็น pipeline ที่มีสามจุดที่พังได้ก่อนจะแตะข้อมูลของคุณเลย: network (ข้อมูลมากเกินไป, req.destroy() แก้ได้), parser (JSON.parse throw, try/catch แก้ได้) และ validation ของคุณเอง (field หายหรือ type ผิด, การเช็คแก้ได้) แต่ละขั้นตอนต้องการ guard และ status code ของตัวเอง — body limit ที่ล้มเหลวคือ 413, parse ที่ล้มเหลวคือ 400, validation ที่ล้มเหลวก็ 400 เช่นกัน (พร้อมข้อความที่เจาะจงกว่า) การยุบทุกอย่างให้เหลือแค่ 500 ทั่วไปไม่บอก client เลยว่าต้องแก้อะไร
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”limit ขนาด byte ที่ hardcode ไว้อย่าง demo 20 byte ด้านบนนั้นง่ายแต่ตามอำเภอใจ — ต่ำเกินไป request ที่ถูกต้องก็โดนปฏิเสธ สูงเกินไปการป้องกันก็แทบไม่ช่วยอะไร API ใน production มักจะคำนวณ limit จากความต้องการจริงของ endpoint (login form ต้องการแค่ byte ไม่ใช่ megabyte) หรือมอบให้ reverse proxy ที่อยู่หน้า Node จัดการ ซึ่งปฏิเสธ body ที่เกินขนาดได้ก่อนจะถึง process ของคุณด้วยซ้ำ — แลกมาด้วย configuration อีกชั้นที่ต้อง sync กับโค้ด application ของคุณ
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “แค่ปฏิเสธ body ที่เกิน limit ด้วย
res.end()ก็พอ ไม่ต้องแตะreq” client อาจยังอยู่กลาง upload — ถ้าไม่มีreq.destroy()Node จะยังอ่าน (และ buffer) byte ที่ไม่มีวันถูกใช้ต่อไป ทำลายจุดประสงค์ของ limit ทั้งหมด - “JSON.parse error หายากพอที่จะข้าม try/catch ได้” client ไหนก็ตาม — integration ที่มี bug, คำสั่ง curl ที่พิมพ์มือ, request ที่ retry แล้ว body ขาด — ส่ง JSON ที่ invalid ได้ทั้งนั้น มันพบบ่อยพอ ๆ กับ input ผิดรูปแบบอื่น ๆ และไม่ถูก catch โดย default
- “
.lengthใกล้เคียงพอกับจำนวน byte จริงสำหรับ header Content-Length” มันถูกต้องเป๊ะสำหรับ ASCII และผิดแบบเงียบ ๆ สำหรับทุกอย่างที่เหลือ — อาการ (response body ถูกตัดทิ้ง) จะโผล่มาให้เห็นก็ต่อเมื่อข้อความจริงที่มีตัวอักษรไม่ใช่ ASCII ไหลผ่าน code path นั้น
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”body reader ที่ AI เขียนมักจะ implement pattern การเก็บ data/end ได้ถูกต้อง และก็มักจะข้ามสิ่งเดียวที่ pattern นั้นต้องการเพื่อให้ปลอดภัยจาก client ที่เป็นอันตรายหรือมี bug: size limit
🐛 หา bug ในโค้ดนี้
function readBody(req) {return new Promise(function (resolve, reject) { let raw = ''; req.on('data', function (chunk) { raw += chunk; }); req.on('end', function () { resolve(raw); }); req.on('error', reject);});}// Called directly on every incoming request, with no size check anywhereเฉลย
readBody นี้จะ concatenate chunk ได้ไม่จำกัดจำนวน — client ที่ stream body หลาย gigabyte (โดยบังเอิญหรือตั้งใจ) จะทำให้ string raw โตขึ้นตลอดเวลา ไม่มีขอบเขตบน ค้าง memory และ event loop จนกว่า process จะ memory หมดหรือ connection ถูกฆ่าจากภายนอก ทางแก้คือ pattern ที่แสดงไว้ก่อนหน้านี้ในบทนี้: ติดตามจำนวน byte สะสมขณะที่ chunk เข้ามา แล้วเมื่อเกิน limit ให้ตอบ 413 Payload Too Large และเรียก req.destroy() เพื่อหยุดอ่านทันที แทนที่จะเชื่อว่า client จะส่ง event end มาในที่สุด