ข้ามไปยังเนื้อหา

Routing

Routing คือกระบวนการตัดสินใจว่า code ส่วนใดจะจัดการ HTTP request ที่เข้ามา โดยพิจารณาจากสอง property ของ request:

  • req.method — HTTP verb เช่น GET, POST, PUT, DELETE
  • req.url — path (และ query string ถ้ามี) เช่น /users, /users/42, /search?q=node

server ของ Node.js มีทั้งสองอย่างให้พร้อม คุณเพียงแค่เขียน logic ที่ map ทั้งสองไปยัง handler

router ที่ง่ายที่สุดคือ chain ของ if statement:

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

เมื่อ URL มี path parameter หรือ query string ให้ใช้ global URL constructor ของ Node เพื่อ parse อย่างสะอาดสะอ้าน ส่ง req.url เป็น argument แรกและ base ปลอมเป็น argument ที่สอง:

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

การเช็ค startsWith ด้วยมือจะพังทันทีที่ route มี parameter Node 24 ทำให้ URLPattern ซึ่งเป็นมาตรฐานเว็บกลายเป็น global คุณจึงประกาศ route อย่าง /users/:id แล้วอ่านค่าที่จับได้ (ส่วน url.parse() แบบเก่า runtime-deprecated ตั้งแต่ Node 24 — ให้ใช้ URL และ URLPattern แทน)

const userRoute = new URLPattern({ pathname: '/users/:id' });
const match = userRoute.exec('http://localhost:3000/users/42');
console.log(match.pathname.groups.id); // '42'
console.log(userRoute.test('http://localhost:3000/posts/1')); // false

route หนึ่งอาจมีอยู่จริงสำหรับ path หนึ่ง แต่ไม่รองรับ method ที่ client ใช้ — นี่เป็นสถานการณ์ที่ต่างจาก path ที่ไม่มีอยู่เลย และควรได้ status code ที่ต่างกัน: 404 แปลว่า “ไม่มี path นี้” ส่วน 405 Method Not Allowed แปลว่า “path นี้มีอยู่ แต่ไม่รองรับ verb นี้” (และควรมี header Allow บอกว่ารองรับ method อะไรบ้าง):

Node.js
Output (verified on Node 24)
GET /users/42: { status: 200, body: { id: '42' } }
DELETE /users/42: { status: 405, allow: 'GET' }
GET /unknown: { status: 404 }

/users/42 match กับ pattern ทั้งสำหรับ GET และ DELETE — ความต่างอยู่แค่ในการ lookup ที่สอง คือ route.methods[req.method] นั่นแหละคือสิ่งที่ทำให้ 405 เป็นไปได้ตรงนี้: โค้ดรู้อยู่แล้วว่า path match ตั้งแต่ก่อนที่จะพบว่า method ไม่ match

เมื่อมี route มากกว่าสองสามอัน ควร extract ออกมาเป็น router map pattern นี้คือสิ่งที่ Express ใช้ภายใน:

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

router คือตาราง lookup ที่มีสอง key ซึ่งเช็คตามลำดับที่แน่นอน: เช็ค path ก่อน (มีอะไร match รูปแบบ URL นี้บ้างไหม?) แล้วค่อยเช็ค method (สิ่งที่ match นั้นรองรับ verb นี้ไหม?) การเช็คทั้งสองพร้อมกันด้วย key รวมตัวเดียว — 'GET /users' เป็น string เดียว — ยุบการตัดสินใจสองขั้นตอนนั้นให้เหลือขั้นตอนเดียว ซึ่งนั่นแหละคือเหตุผลที่ tiny router object ด้านบนแยก “ไม่มี path นี้” กับ “verb ผิดสำหรับ path นี้” ไม่ออก: ทั้งคู่ตกลงไปที่ branch else เดียวกัน

การ lookup ด้วย key รวม (method + ' ' + path) ใช้โค้ดน้อยที่สุดและอ่านง่ายมากสำหรับ route คงที่ไม่กี่อัน — จนกว่าคุณจะต้องการ parameter (/users/:id) หรือ response 405 ตอนนั้นคุณต้อง match path กับ method เป็นสองขั้นตอนแยกกัน ซึ่งเป็นโค้ดมากกว่า map เดิม URLPattern ให้การจับ parameter จริงและโครงสร้างสองขั้นตอนแบบฟรี ๆ แต่มันเป็น global ที่ใหม่กว่า (Node 24+) — route table แบบในบทนี้ย้ายไป Node เวอร์ชันไหนหรือ router ของ framework ไหนก็ได้ง่าย ๆ ในขณะที่โค้ด URLPattern ยังไม่ใช่สิ่งที่คุณสมมติได้ว่าทุก runtime รองรับ

  • “404 คือ response ที่ถูกต้องเสมอเมื่อ route lookup ล้มเหลว” จริงเฉพาะเมื่อ path เองไม่ match เลย ถ้า path match แต่ method ไม่รองรับ status code ที่ถูกต้องคือ 405 พร้อม header Allow — 404 บอก client ว่า “เลิกลอง URL นี้” ส่วน 405 บอกว่า “ลอง verb อื่นที่นี่”
  • “parameter ของ URLPattern ถูก validate ให้อัตโนมัติ” มันถูกจับมาเป็น string เฉย ๆ ไม่มีอะไรมากกว่านั้น — /users/:id match กับ /users/abc ได้พอ ๆ กับ /users/42 การ validate ว่า group ที่จับมาหน้าตาเหมือน ID จริงยังเป็นหน้าที่ของ handler อยู่ดี
  • “router ที่เขียนเองมี fragile กว่า router ของ framework โดยธรรมชาติ” การตัดสินใจพื้นฐาน (match path แล้วค่อย match method) เหมือนกันทั้งคู่ — framework ส่วนใหญ่แค่เพิ่มความสะดวก (middleware, การแปลง parameter, wildcard/catch-all route) บน lookup สองขั้นตอนแบบเดียวกัน

router ที่ AI เขียนมักจะ key route ด้วย method + path เป็น string เดียว ซึ่งอ่านง่ายสำหรับ happy path และให้ status code ผิดแบบเงียบ ๆ ทันทีที่ path ถูกเรียกด้วย method ที่ไม่รองรับ

🐛 หา bug ในโค้ดนี้

const routes = {
'GET /users': listUsers,
'POST /users': createUser,
};
const server = http.createServer(function (req, res) {
const key = req.method + ' ' + new URL(req.url, 'http://x').pathname;
const handler = routes[key];
if (handler) {
handler(req, res);
} else {
res.writeHead(404);
res.end('Not found');
}
});
// A DELETE request to /users returns 404, as if the route didn't exist at all
เฉลย

/users เป็น resource จริง — GET และ POST ถูกกำหนดไว้ทั้งคู่ — แต่ DELETE /users ไม่มีอยู่ใน map เลยตกไปที่ branch else เดียวกับ request ไปยัง path ที่ไม่เคยถูกกำหนดไว้เลย เช่น /completely-unknown ทั้งคู่ได้ 404 ซึ่งบอก client ว่า path เองผิด client ไม่มีทางรู้เลยว่า path จริง ๆ ถูกต้อง มีแค่ verb เท่านั้นที่เป็นปัญหา ทางแก้คือ lookup ด้วย path ก่อน แล้วเช็คว่า route ที่ match รองรับ method ของ request หรือไม่ — คืน 405 พร้อม header Allow เมื่อไม่รองรับ เหมือนตัวอย่าง URLPattern ก่อนหน้านี้ในบทนี้เป๊ะ

สอง property ของ request object ที่ใช้ตัดสินใจ routing คืออะไร?
new URL(req.url, "http://localhost") ให้อะไรคุณ?
router ควรคืน HTTP status code ใดเมื่อไม่มี route ตรงกัน?
มี route สำหรับ /users/:id ที่รองรับแค่ GET client ส่ง DELETE /users/42 มา server ควรตอบด้วยอะไร?
const match = route.pattern.exec(url);
const handler = route.methods[req.method];
// route.methods = { GET: getUser }
ทำไมการ key route table ด้วย req.method + ' ' + pathname เป็น string รวมตัวเดียว ถึงทำให้คืน 405 ไม่ได้เลย?