Secure Backend Routing & RESTful API Standards
"The Server-Side Mental Model assumes the client is untrustworthy" — frontend คือหน้าร้านที่ช่วยผู้ใช้ ส่วน backend คือห้องนิรภัยที่ไม่เชื่อใครทั้งนั้น
Server-Side Mental Model
| Frontend (The Concierge) | Backend (The Gatekeeper) | |
|---|---|---|
| เป้าหมายของ validation | UX — บอกผู้ใช้เร็ว ๆ ว่ากรอกผิด | Survival — ความถูกต้องและความปลอดภัยของข้อมูล |
| สมมติฐาน | ผู้ใช้เป็นคนดี แค่พิมพ์พลาด | ทุก request อาจเป็นผู้ไม่หวังดี |
| Global object | window | global |
| งานหลัก | render UI / โต้ตอบ | ประมวลผลข้อมูล / จัดการไฟล์ |
| ความปลอดภัย | เปิด — ผู้ใช้เห็นโค้ดทั้งหมด | ปิด — ผู้ใช้เห็นแค่ API |
| ประสิทธิภาพ | ถูกจำกัดด้วยเครื่องของผู้ใช้ | ขยายได้ด้วยการเพิ่มกำลัง server |
หลักการ REST
REST (Representational State Transfer) = รูปแบบสถาปัตยกรรมมาตรฐานสำหรับ web service ที่สื่อสารแบบ stateless ผ่าน HTTP/HTTPS
| หลักการ | ความหมาย |
|---|---|
| Resources & URIs | จัดข้อมูลเป็นทรัพยากร เข้าถึงผ่าน URI เฉพาะ เช่น /users/1 |
| HTTP Methods | GET อ่าน, POST สร้าง, PUT/PATCH แก้, DELETE ลบ |
| Statelessness | ทุก request มีข้อมูลครบในตัวเอง server ไม่เก็บ session ของ client → ขยายระบบง่าย |
| JSON/XML | แลกเปลี่ยนข้อมูลด้วยรูปแบบเบา ๆ ปกติคือ JSON |
การตั้งชื่อ endpoint — ข้อสอบถามแน่
| Action-based (ผิดหลัก REST) | Resource-based (ถูก) | |
|---|---|---|
| ดูสินค้าทั้งหมด | GET /getProducts | GET /api/products |
| ดูสินค้า 1 ชิ้น | GET /getProductById?id=1 | GET /api/products/1 |
| เพิ่มสินค้า | POST /createProduct | POST /api/products |
| ลบสินค้า | POST /deleteProduct | DELETE /api/products/1 |
Statelessness
ฝั่ง frontend ตัวแปรอยู่ในหน่วยความจำตราบที่หน้ายังเปิด แต่ฝั่ง backend server ลืมทุกอย่างทันทีที่ส่ง response เสร็จ
Mental model จากสไลด์
เหมือนคุยกับเพื่อนที่ความจำเสื่อมสนิท — ต้องแนะนำตัวใหม่ทุกครั้ง
ดังนั้นทุก request ต้องแนบ token หรือ ID ไปด้วย เช่น GET /profile?user=123
หรือแนบ header Authorization: Bearer <token>
Express Routing
const express = require('express');
const app = express();
app.use(express.json()); // middleware ต้องมาก่อน routes
app.get('/api/products', (req, res) => {
res.status(200).json(products);
});
app.get('/api/products/:id', (req, res) => {
const item = products.find(p => p.id == req.params.id);
if (!item) {
return res.status(404).json({ error: 'Product not found' });
}
res.status(200).json(item);
});
app.listen(3000, () => console.log('Server running on port 3000'));
| สิ่งที่ต้องอ่านจาก request | เขียนอย่างไร | ตัวอย่าง URL |
|---|---|---|
| Route parameter | req.params.id | /api/products/7 |
| Query string | req.query.category | /api/products?category=hat |
| Body (POST/PUT) | req.body.email | ข้อมูลอยู่ในตัว request ไม่อยู่ใน URL |
Middleware & express.json()
express.json() อ่าน stream แล้วรัน JSON.parse() ภายใน จากนั้นแนบผลลัพธ์เป็น req.body
- เรียกว่า middleware เพราะอยู่ "ตรงกลาง" ของเส้นทาง request
- ต้องประกาศ ก่อน routes เสมอ ไม่งั้นข้อมูลยังไม่ถูกแกะตอน route ทำงาน
- ทำงานเมื่อเห็น header
Content-Type: application/json
3 เฟสของ Body Parsing
| เฟส | เกิดที่ไหน | ข้อมูลอยู่ในรูปแบบใด |
|---|---|---|
| 1. Serialized String | Frontend — JSON.stringify(orderData) | utf-8 text stream + header Content-Type: application/json |
| 2. Middleware Buffer | app.use(express.json()) | อ่าน stream แล้ว rehydrate ด้วย JSON.parse() สร้าง property body บน req |
| 3. Populated Object | ใน route handler | JavaScript object เต็มรูปแบบ ใช้ req.body.price ได้เลย |
app.post('/api/checkout', (req, res) => {
console.log(req.body);
// { productId: 101, quantity: 2, price: 50 }
const total = req.body.quantity * req.body.price;
});
Validation & RegEx ฝั่ง server
RegEx ไม่ได้ใช้เพื่อความสวยงามของ UI แต่ใช้เป็น logical firewall รักษาความถูกต้องของข้อมูล
// อีเมล const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(req.body.email)) { return res.status(400).json({ error: 'Invalid email format' }); } // เลขบัตร 16 หลัก const cardRegex = /^[0-9]{16}$/; // รหัสผ่าน: อย่างน้อย 8 ตัว มีตัวใหญ่ 1 ตัว และอักขระพิเศษ 1 ตัว const passRegex = /^(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,}$/;
<script> ต้อง sanitize ก่อนบันทึกลงฐานข้อมูลเพื่อกัน XSS3 ชั้นของ Gatekeeper
| ชั้น | คำถาม | เหตุผลเชิงธุรกิจ |
|---|---|---|
| 1. Authentication | "คุณคือใคร" | ให้เฉพาะผู้ใช้ที่ลงทะเบียนสั่งซื้อได้ |
| 2. Authorization | "คุณได้รับอนุญาตให้ทำสิ่งนี้ไหม" | ลูกค้าต้องลบสินค้าออกจาก catalog ไม่ได้ |
| 3. Validation | "ข้อมูลนี้สมเหตุสมผลไหม" | กันการสั่งเสื้อ -5 ตัว หรือโน้ตบุ๊กราคา $0 |
CORS
เมื่อ frontend (origin หนึ่ง) เรียก backend (อีก origin) เบราว์เซอร์จะบล็อกไว้ก่อน
const cors = require('cors');
app.use(cors({ origin: 'https://myshop.example.com' }));
Contract Table
| Component | Request (The Order) | Response (The Delivery) |
|---|---|---|
| Method | GET | — |
| Endpoint | /api/products | — |
| Headers | Accept: application/json | Content-Type: application/json |
| Status Code | — | 200 OK (หรือ 404 Not Found) |
| Body | ว่าง (สำหรับ GET) | [{"id":1,"name":"Laptop","price":999}, ...] |
แนวข้อสอบอัตนัย
โจทย์: ทีมงานรายงานว่า POST /api/checkout อ่าน req.body ได้เป็น undefined ตลอด
ทั้งที่ frontend ส่งข้อมูลไปจริง จงวิเคราะห์
- ปัญหา — route handler เข้าถึงข้อมูลใน body ไม่ได้ ทำให้ order ถูกปฏิเสธหรือ server พังทุกครั้ง
- สาเหตุทางเทคนิค — เป็นไปได้ 2 อย่าง: (ก) ไม่ได้ประกาศ
app.use(express.json())หรือประกาศหลัง route ทำให้ middleware ไม่ทันแกะ stream (ข) ฝั่ง client ไม่ได้ตั้ง headerContent-Type: application/jsonmiddleware จึงข้ามการ parse - ผลกระทบ — checkout ใช้ไม่ได้ทั้งระบบ ลูกค้าสั่งซื้อไม่สำเร็จ = ธุรกิจเสียรายได้ และถ้าโค้ดไม่ได้ดัก null จะเกิด 500 error ซ้ำ ๆ
- วิธีแก้เชิงสถาปัตยกรรม — ย้าย
app.use(express.json())ขึ้นไปไว้เหนือ routes ทั้งหมด, บังคับ header ฝั่ง client, และเพิ่ม guard ใน route ว่าถ้า!req.bodyหรือฟิลด์จำเป็นหายให้ตอบ 400 พร้อมระบุฟิลด์ที่ผิด แทนที่จะปล่อยให้ crash เป็น 500