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)
เป้าหมายของ validationUX — บอกผู้ใช้เร็ว ๆ ว่ากรอกผิดSurvival — ความถูกต้องและความปลอดภัยของข้อมูล
สมมติฐานผู้ใช้เป็นคนดี แค่พิมพ์พลาดทุก request อาจเป็นผู้ไม่หวังดี
Global objectwindowglobal
งานหลักrender UI / โต้ตอบประมวลผลข้อมูล / จัดการไฟล์
ความปลอดภัยเปิด — ผู้ใช้เห็นโค้ดทั้งหมดปิด — ผู้ใช้เห็นแค่ API
ประสิทธิภาพถูกจำกัดด้วยเครื่องของผู้ใช้ขยายได้ด้วยการเพิ่มกำลัง server
คำถามสำคัญจากสไลด์: ถ้าผู้ใช้แก้ราคาสินค้าเป็น $0 ใน Browser Console แล้วกด Buy ใครรับผิดชอบในการจับ — คำตอบคือ backend เท่านั้น เพราะทุกอย่างฝั่ง client ถูกแก้ได้หมด

หลักการ REST

REST (Representational State Transfer) = รูปแบบสถาปัตยกรรมมาตรฐานสำหรับ web service ที่สื่อสารแบบ stateless ผ่าน HTTP/HTTPS

หลักการความหมาย
Resources & URIsจัดข้อมูลเป็นทรัพยากร เข้าถึงผ่าน URI เฉพาะ เช่น /users/1
HTTP MethodsGET อ่าน, POST สร้าง, PUT/PATCH แก้, DELETE ลบ
Statelessnessทุก request มีข้อมูลครบในตัวเอง server ไม่เก็บ session ของ client → ขยายระบบง่าย
JSON/XMLแลกเปลี่ยนข้อมูลด้วยรูปแบบเบา ๆ ปกติคือ JSON

การตั้งชื่อ endpoint — ข้อสอบถามแน่

Action-based (ผิดหลัก REST)Resource-based (ถูก)
ดูสินค้าทั้งหมดGET /getProductsGET /api/products
ดูสินค้า 1 ชิ้นGET /getProductById?id=1GET /api/products/1
เพิ่มสินค้าPOST /createProductPOST /api/products
ลบสินค้าPOST /deleteProductDELETE /api/products/1
ทำไม resource ดีกว่าเมื่อต้อง scale: คำกริยาอยู่ที่ HTTP method อยู่แล้ว ใช้ชื่อ resource ทำให้ URL คาดเดาได้ ทำ caching และ routing ได้เป็นระบบ และเพิ่ม action ใหม่โดยไม่ต้องสร้าง URL ใหม่ทุกครั้ง

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 parameterreq.params.id/api/products/7
Query stringreq.query.category/api/products?category=hat
Body (POST/PUT)req.body.emailข้อมูลอยู่ในตัว request ไม่อยู่ใน URL
Modular architecture: แยก Routes (URL) ออกจาก Controllers (logic) เพื่อเลี่ยงปัญหา "God File" ที่ไฟล์เดียวทำทุกอย่าง

Middleware & express.json()

Client JSON.stringify raw string express.json() middleware — "unpacker" req.body app.post(...) route handler Request เดินทางผ่าน middleware ก่อนถึง route เสมอ '{"productId":101,"quantity":2}' → { productId: 101, quantity: 2 } ถ้าวาง app.post ไว้เหนือ app.use(express.json()) → req.body เป็น undefined ถ้า client ลืมตั้ง header Content-Type: application/json → unpacker ไม่ทำงาน

express.json() อ่าน stream แล้วรัน JSON.parse() ภายใน จากนั้นแนบผลลัพธ์เป็น req.body

3 เฟสของ Body Parsing

เฟสเกิดที่ไหนข้อมูลอยู่ในรูปแบบใด
1. Serialized StringFrontend — JSON.stringify(orderData)utf-8 text stream + header Content-Type: application/json
2. Middleware Bufferapp.use(express.json())อ่าน stream แล้ว rehydrate ด้วย JSON.parse() สร้าง property body บน req
3. Populated Objectใน route handlerJavaScript 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,}$/;
Sanitization ต่างจาก Validation: ข้อมูลอาจ "ถูกรูปแบบ" (เป็นอีเมลจริง) แต่ยัง "อันตราย" ได้ เช่นมีแท็ก <script> ต้อง sanitize ก่อนบันทึกลงฐานข้อมูลเพื่อกัน XSS

3 ชั้นของ Gatekeeper

ชั้นคำถามเหตุผลเชิงธุรกิจ
1. Authentication"คุณคือใคร"ให้เฉพาะผู้ใช้ที่ลงทะเบียนสั่งซื้อได้
2. Authorization"คุณได้รับอนุญาตให้ทำสิ่งนี้ไหม"ลูกค้าต้องลบสินค้าออกจาก catalog ไม่ได้
3. Validation"ข้อมูลนี้สมเหตุสมผลไหม"กันการสั่งเสื้อ -5 ตัว หรือโน้ตบุ๊กราคา $0

CORS

เมื่อ frontend (origin หนึ่ง) เรียก backend (อีก origin) เบราว์เซอร์จะบล็อกไว้ก่อน

บทเรียนจากสไลด์: CORS เป็น ฟีเจอร์ความปลอดภัย ไม่ใช่บั๊ก ต้องตั้งค่าให้ backend "ไว้ใจ" frontend ของเราอย่างเจาะจง ไม่ใช่เปิดกว้างให้ทุก origin
const cors = require('cors');
app.use(cors({ origin: 'https://myshop.example.com' }));

Contract Table

ComponentRequest (The Order)Response (The Delivery)
MethodGET—
Endpoint/api/products—
HeadersAccept: application/jsonContent-Type: application/json
Status Code—200 OK (หรือ 404 Not Found)
Bodyว่าง (สำหรับ GET)[{"id":1,"name":"Laptop","price":999}, ...]
Contract table + sequence diagram = สิ่งที่ต้องเขียนก่อนสั่ง GenAI เขียนโค้ด ไม่งั้น AI จะเขียนโค้ดที่ดีให้กับสเปกที่ผิด

แนวข้อสอบอัตนัย

โจทย์: ทีมงานรายงานว่า POST /api/checkout อ่าน req.body ได้เป็น undefined ตลอด ทั้งที่ frontend ส่งข้อมูลไปจริง จงวิเคราะห์

  1. ปัญหา — route handler เข้าถึงข้อมูลใน body ไม่ได้ ทำให้ order ถูกปฏิเสธหรือ server พังทุกครั้ง
  2. สาเหตุทางเทคนิค — เป็นไปได้ 2 อย่าง: (ก) ไม่ได้ประกาศ app.use(express.json()) หรือประกาศหลัง route ทำให้ middleware ไม่ทันแกะ stream (ข) ฝั่ง client ไม่ได้ตั้ง header Content-Type: application/json middleware จึงข้ามการ parse
  3. ผลกระทบ — checkout ใช้ไม่ได้ทั้งระบบ ลูกค้าสั่งซื้อไม่สำเร็จ = ธุรกิจเสียรายได้ และถ้าโค้ดไม่ได้ดัก null จะเกิด 500 error ซ้ำ ๆ
  4. วิธีแก้เชิงสถาปัตยกรรม — ย้าย app.use(express.json()) ขึ้นไปไว้เหนือ routes ทั้งหมด, บังคับ header ฝั่ง client, และเพิ่ม guard ใน route ว่าถ้า !req.body หรือฟิลด์จำเป็นหายให้ตอบ 400 พร้อมระบุฟิลด์ที่ผิด แทนที่จะปล่อยให้ crash เป็น 500