Skip to content

บทที่ 5 — HTTP + Web Server: จาก http เปล่า ๆ สู่ Express

← บทที่ 4: Filesystem + Streams | สารบัญ | บทที่ 6: Error Handling + Debugging →

นี่คือบทที่ทำให้ Node เป็น "ภาษาของ backend" จริง ๆ และเป็นสะพานตรงสู่ NestJS เราจะ:

  1. สร้าง HTTP server จาก core module http เปล่า ๆ — เห็นทุกอย่างที่เกิดขึ้น
  2. ทำ routing + อ่าน request body + ตอบ JSON ด้วยมือ — แล้ว รู้สึกถึงความเจ็บปวด
  3. เห็นว่า Express แก้ความเจ็บนั้นยังไง + middleware คืออะไร
  4. เชื่อมโยงว่า Express → NestJS ต่อยอดกันยังไง
  5. เป็น HTTP client เรียก API อื่นด้วย fetch

💡 คำว่า client/server ที่จะเจอบ่อยในบทนี้ — client = ฝั่งที่ "เรียกเข้ามา" (เบราว์เซอร์, mobile app, หรือ server อีกตัวที่เรียก API เรา); server = ฝั่งที่ "ตอบ" (โค้ดที่เรากำลังเขียน) · ในข้อ 6 บทบาทจะสลับ — Node ของเราเป็น client ไปเรียก API อื่น

📝 Fastify เป็นทางเลือกที่เร็วกว่า Express — ถ้างานเน้น throughput สูง (benchmark เร็วกว่า ~2–3 เท่า) ลองดู Fastify · NestJS 11 รองรับ Fastify adapter ในตัว (เปลี่ยน 1 บรรทัดตอน bootstrap) บทนี้ใช้ Express เพราะเป็นมาตรฐานที่บริษัทไทยใช้กันมากที่สุด

💡 ทำไมต้องเขียน server จาก http เปล่า ก่อนใช้ framework? เพราะ Express/Nest แค่ "ห่อ" http ไว้ — ถ้าเข้าใจชั้นล่างสุด คุณจะ debug framework ออกและไม่ใช้แบบงม ๆ


1. สร้าง server จาก http เปล่า ๆ

javascript
// server.mjs
import { createServer } from "node:http";

// createServer รับ handler ที่ถูกเรียก "ทุกครั้งที่มี request เข้ามา"
const server = createServer((req, res) => {
  // req = IncomingMessage — ข้อมูล request (ที่จริงเป็น Readable stream! บทที่ 4)
  // res = ServerResponse  — ตัวเขียนคำตอบ (ที่จริงเป็น Writable stream!)
  console.log(`${req.method} ${req.url}`);   // เช่น "GET /users"

  res.statusCode = 200;                                  // HTTP status
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
  res.end("Hello from raw http!");                       // ส่งคำตอบ + ปิด connection
});

// 🚀 โซนขั้นสูง — ข้ามได้รอบแรก: 4 บรรทัดนี้เป็นการตั้งค่า timeout สำหรับ production
// จริง (กัน Slowloris DoS + เข้ากันได้กับ load balancer) ถ้าแค่หัดสร้าง server ตัวแรก
// ข้ามไปดูส่วน server.listen(...) ข้างล่างได้เลย แล้วกลับมาอ่านตอนต้องขึ้น production จริง
server.keepAliveTimeout = 65_000;        // เก็บ TCP connection ไว้ใช้ซ้ำได้นานเท่าไร (ms)
server.headersTimeout   = 66_000;        // ต้องมากกว่า keepAliveTimeout เล็กน้อย (Node เช็คตามลำดับ)
server.requestTimeout   = 30_000;        // เวลามากสุดต่อ 1 request — กัน client ส่งช้า ๆ ลากยาว (Slowloris)
server.maxRequestsPerSocket = 100;       // ปิด socket หลังตอบครบ N request — กัน socket exhaustion

// ฟังที่ port 3000 (server เป็น EventEmitter — บทที่ 3)
server.listen(3000, () => console.log("http://localhost:3000"));

จุดที่ต้องเก็บไว้ในใจ: req คือ Readable stream, res คือ Writable stream (บทที่ 4) — นั่นคือเหตุผลที่ Node ส่งไฟล์ใหญ่/วิดีโอผ่าน HTTP ได้โดยไม่ระเบิด RAM (pipe ไฟล์เข้า res ตรง ๆ ได้)

🛡️ ทำไม timeout 4 ตัวสำคัญสำหรับ production:

  • keepAliveTimeout / headersTimeout — ป้องกัน Slowloris attack (ชื่อการโจมตีแบบหนึ่ง) วิธีการคือ attacker (ผู้โจมตี) เปิด connection ค้างไว้เฉย ๆ แล้วส่ง header ทีละไบต์ช้า ๆ จนกว่า server จะหมด socket เหลือไว้รับคนอื่น
  • requestTimeout (default ใน Node 18+ = 5 นาที — ตรวจสอบค่า default ล่าสุดของเวอร์ชัน Node ที่ใช้จริงในเอกสารทางการก่อนใช้ เพราะแต่ละเวอร์ชันอาจปรับค่าไม่เท่ากัน) — กัน body ค้าง
  • maxRequestsPerSocket — กัน connection ตัวเดียวยึดทรัพยากรนาน + บังคับให้ load balancer rotate connection
  • ถ้าวาง Node หลัง load balancer (= ตัวกระจาย request ไปหลาย server เพื่อแบ่งโหลด เช่น AWS ALB ของ Amazon หรือ nginx ซอฟต์แวร์ยอดนิยมที่ทำหน้าที่นี้ได้ด้วย) ต้องตั้ง keepAliveTimeout ของ Node ให้ มากกว่า idle timeout ของ LB ไม่งั้นจะเจอ 502 แบบ random (LB ส่ง request เข้า socket ที่ Node เพิ่งปิด)

HTTP status code — เลขที่บอก "ผลลัพธ์ของ request"

โค้ดข้างบนตอบ 200 — เลขพวกนี้ (HTTP status code) คือตัวเลขมาตรฐานที่ server (ฝั่งที่ตอบ — โค้ดเรา) บอก client (ฝั่งที่เรียกเข้ามา — เบราว์เซอร์/แอป/อีก server) ว่า request นั้น "ผลเป็นไง" ตั้งแต่บทนี้ไปจะเจอเลขดิบ ๆ เยอะ (ในบท 6, 9 ด้วย) จำกลุ่มหลัก ๆ ไว้ — เลขขึ้นต้น 2 = สำเร็จ, 4 = client ส่งมาผิด, 5 = server พัง:

codeแปลว่าใช้ตอนไหน
200OK — สำเร็จGET/อัปเดตสำเร็จ ตอบข้อมูลกลับ
201Created — สร้างแล้วPOST สร้างของใหม่สำเร็จ
400Bad Request — client ส่งผิดbody/param ผิดรูปแบบ, validate ไม่ผ่าน
401Unauthorized — ยังไม่ loginไม่ได้ส่ง token/ยังไม่ยืนยันตัวตน
404Not Found — ไม่เจอไม่มี route/ของชิ้นนั้น
500Internal Server Error — server พังบั๊กในโค้ดเรา หรือ dependency ล่มแบบไม่ได้คาด (เช่น DB query throw)
503Service Unavailable — ยังไม่พร้อม / overloadedserver จงใจ บอกว่าไม่พร้อมรับงาน เช่น maintenance, กำลัง warm-up, หรือ k8s readiness probe ยังไม่ผ่าน

📝 500 vs 503 — สับสนบ่อย: ถ้า DB ล่มแบบไม่คาด → 500 (บั๊ก/error ที่หลุดออกมา) · ถ้าเรา ตรวจ DB เองแล้วรู้ว่าล่ม จึงตอบ "ยังไม่พร้อม" เพื่อให้ load balancer / k8s probe เอา instance นี้ออกจาก rotation → 503 · ความต่างนี้สำคัญสำหรับ k8s readiness probe (ตอบ 503 = ไม่รับ traffic เพิ่ม แต่ตัวเองยังไม่ตาย) — รายละเอียดในบทที่ 9


2. ทำเองทุกอย่าง — และรู้สึกถึงความเจ็บ

ลองทำ REST API เล็ก ๆ จัดการ "users" ด้วย http เปล่า — ดูว่าต้องทำเองกี่อย่าง:

javascript
// raw-api.mjs
import { createServer } from "node:http";

const users = [{ id: 1, name: "สมชาย" }];

const server = createServer(async (req, res) => {
  // ✋ งานที่ 1: ตอบเป็น JSON ต้อง setHeader + JSON.stringify เองทุกครั้ง
  const json = (status, data) => {
    res.statusCode = status;
    res.setHeader("Content-Type", "application/json; charset=utf-8");
    res.end(JSON.stringify(data));
  };

  // ✋ งานที่ 2: routing ต้อง if/else เทียบ method + url เอง
  if (req.method === "GET" && req.url === "/users") {
    return json(200, users);
  }

  if (req.method === "POST" && req.url === "/users") {
    // ✋ งานที่ 3: อ่าน request body ต้องเก็บ chunk จาก stream เองทีละชิ้น!
    // ⚠️ ห้ามใช้ `body += chunk` กับ string — chunk เป็น Buffer การ implicit toString()
    //    ทีละ chunk จะตัดกลาง code point ของภาษาไทย/emoji (UTF-8 หลายไบต์) พัง
    //    (code point = หน่วยตัวอักษร 1 ตัวใน Unicode — ภาษาไทย/emoji ใน UTF-8 ใช้ 3–4 byte ต่อ 1 ตัว
    //     ถ้า chunk ตัดกลางจะได้ "?" หรือ U+FFFD แทน)
    //    วิธีที่ถูกคือสะสม Buffer ก่อนแล้ว toString('utf8') ครั้งเดียวตอนจบ
    const chunks = [];
    let totalSize = 0;
    const MAX_BODY = 1 * 1024 * 1024;   // 1 MB — guard กัน DoS (ของจริงต้อง configurable)
    for await (const chunk of req) {     // req เป็น Readable stream
      totalSize += chunk.length;
      if (totalSize > MAX_BODY) {
        req.destroy();                   // ⭐ ปิด stream + บอก client หยุดส่ง (back-pressure)
        return json(413, { error: "body ใหญ่เกิน" });
      }
      chunks.push(chunk);
    }
    const body = Buffer.concat(chunks).toString("utf8");
    // ✋ งานที่ 4: parse JSON เอง + จัดการกรณี JSON พัง
    let parsed;
    try {
      parsed = JSON.parse(body);
    } catch {
      return json(400, { error: "JSON ไม่ถูกต้อง" });
    }
    // ✋ งานที่ 5: validate เอง
    if (!parsed.name) return json(400, { error: "ต้องมี name" });

    const user = { id: users.length + 1, name: parsed.name };
    users.push(user);
    return json(201, user);
  }

  // ✋ งานที่ 6: 404 ต้องจัดการเอง
  json(404, { error: "ไม่เจอ route นี้" });
});

server.listen(3000, () => console.log("http://localhost:3000"));

ทดสอบ (curl = เครื่องมือยิง request จาก command line — แทนการเปิดเบราว์เซอร์ ใช้พิมพ์คำสั่งใน terminal ส่ง request ไปหา server ได้เลย · -X POST = ระบุ method, -H = ใส่ header, -d = ส่ง body):

bash
curl http://localhost:3000/users
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"สมหญิง"}'

สังเกตความเจ็บ: แค่ 2 endpoint ก็ต้องทำเอง 6 อย่าง — ตอบ JSON, routing, อ่าน body, parse, validate, 404 ลองนึกภาพ 50 endpoint + path parameter (/users/:id) + query string + auth — โค้ดจะกลายเป็น if/else พันบรรทัด นี่คือ ปัญหาที่ framework เกิดมาแก้


3. Express — ความเจ็บหายไปไหน

Express คือ framework ที่บางและเก่าแก่ที่สุดของ Node — มันยังใช้ http ข้างใต้ แต่ห่อให้สะดวก ติดตั้ง:

📌 ตัวอย่างนี้รันได้ทั้ง Express 4 และ Express 5 (Express 5 GA ปลายปี 2024) — ข้อต่างหลัก: Express 5 จับ error จาก async handler ให้อัตโนมัติ (ใน 4 ต้อง try/catch + next(err) เอง) — รายละเอียดในบทที่ 6 (last reviewed: 2026-06 — ตรวจ release notes ทางการก่อนใช้)

bash
npm install express
# production essentials (ดูบทที่ 11 เพิ่มเติม):
npm install helmet cors express-rate-limit

โค้ดเดียวกันด้วย Express:

javascript
// express-api.mjs
import express from "express";
import helmet from "helmet";              // ตั้ง security header (X-Frame-Options, CSP ฯลฯ)
import cors from "cors";                  // เปิด/ปิด cross-origin request
import rateLimit from "express-rate-limit"; // กัน brute-force + spam

const app = express();

// ⭐ production middleware ใส่ก่อน route — เรียงตามลำดับสำคัญ
app.use(helmet());                                        // 1) security header
app.use(cors({ origin: "https://myapp.example.com" }));   // 2) อนุญาตเฉพาะ origin ที่ใช้จริง
app.use(rateLimit({                                       // 3) จำกัด req/IP
  windowMs: 60_000,  // ต่อ 1 นาที
  limit: 100,        // 100 req/IP/นาที
  standardHeaders: "draft-7",
  legacyHeaders: false,
}));

// ⭐ จำกัดขนาด body — สำคัญสำหรับกัน DoS (ห้ามใช้ default ของ Express ที่ 100kb สำหรับ JSON ใหญ่)
app.use(express.json({ limit: "100kb" }));                // parse JSON body → req.body
app.use(express.urlencoded({ limit: "100kb", extended: true })); // parse form body

const users = [{ id: 1, name: "สมชาย" }];

// routing ชัดเจน: app.<method>(path, handler) — ไม่ต้อง if/else
app.get("/users", (req, res) => {
  res.json(users);                       // res.json ตอบ JSON + setHeader ให้เอง
});

app.get("/users/:id", (req, res) => {
  // :id = path parameter — Express แยกให้ใน req.params อัตโนมัติ
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) return res.status(404).json({ error: "ไม่เจอ user" });
  res.json(user);
});

app.post("/users", (req, res) => {
  // req.body มาจาก express.json() แล้ว — ไม่ต้องอ่าน stream เอง
  // ⚠️ ตัวอย่างนี้เป็นการ "เช็คความมี" แบบหยาบ ๆ เพื่อสอน routing เท่านั้น
  //    ของจริง: req.body.name อาจเป็น object, array, หรือ string ยาว 10MB ได้ —
  //    client ส่งอะไรมาก็ได้ (zero-trust input) ต้อง validate ชนิด/ขนาด/รูปแบบ
  //    ด้วย **library validate ข้อมูล** เช่น zod (type-safe, นิยมที่สุดในปี 2026, ใช้ในบท 7/9),
  //    ajv (เร็วที่สุด, อิง JSON Schema), หรือ joi (เก่ากว่า, syntax อ่านง่าย)
  //    — รายละเอียดในบทที่ 9 (REST API จริง)
  if (!req.body.name || typeof req.body.name !== "string") {
    return res.status(400).json({ error: "ต้องมี name (string)" });
  }
  const user = { id: users.length + 1, name: req.body.name };
  users.push(user);
  res.status(201).json(user);
});

app.listen(3000, () => console.log("http://localhost:3000"));

เทียบกัน: อ่าน body เอง / parse / setHeader / routing if-else หายหมด — Express จัดการให้ เหลือแต่ logic ที่เราสนใจจริง ๆ

🛡️ production essentials — ทำไมต้องใส่ทุกอย่างข้างบน:

  • helmet — ตั้ง security header (เช่น X-Content-Type-Options, Strict-Transport-Security) ที่ default ของ Express ไม่ใส่ให้
  • cors — ถ้า frontend อยู่ origin อื่น (เช่น app.example.com เรียก api.example.com) ต้องเปิด CORS · ห้ามใส่ origin: "*" ใน production
  • express-rate-limit — กัน brute-force login, spam endpoint แพง · ⚠️ default ใช้ in-memory store ซึ่งจะ "นับใหม่" ทุก instance ถ้ารัน Node หลาย process หลัง load balancer — production ต้องเปลี่ยนเป็น Redis store (rate-limit-redis) ไม่งั้น attacker กระจาย request ข้าม instance ก็ทะลุ limit ได้
  • limit: "100kb" — Express default ของ express.json() คือ 100kb อยู่แล้ว แต่ ระบุชัด เพื่ออ่านโค้ดแล้วเห็น intent · ถ้ารับ upload ไฟล์ใหญ่ ใช้ multer หรือ stream แยก route (อย่าขยาย limit ของ JSON parser)
  • compression (ไม่ใส่ในตัวอย่าง) — gzip response · production ใส่ที่ reverse proxy (nginx/ALB) จะดีกว่า เพราะ Node ไม่ต้องเสีย CPU บีบอัด

4. middleware — แนวคิดที่ต้องเข้าใจก่อนขึ้น NestJS

middleware คือฟังก์ชันที่ทำงาน "คั่นกลาง" ระหว่าง request เข้ามา กับ handler สุดท้าย — ต่อกันเป็น โซ่ request ไหลผ่านทีละตัว แต่ละตัวเลือกว่าจะ "ส่งต่อ" (เรียก next()) หรือ "ตอบเลย" (จบโซ่)

text
Request → [logger] → [auth] → [express.json] → [handler ของเรา] → Response
            │          │           │                  │
          next()     next()      next()            res.json()
javascript
// middleware = function (req, res, next) — เรียก next() เพื่อส่งต่อตัวถัดไป
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();   // ⭐ ต้องเรียก next() ไม่งั้น request ค้าง (ไม่ส่งต่อ ไม่ตอบ)
});

// 🚀 โซนขั้นสูง — ข้ามได้รอบแรก: ตัวอย่างข้างล่างนี้เป็น middleware ตรวจ auth ด้วย JWT จริง
// (มีเรื่อง token, signature, CVE) ถ้าเพิ่งเข้าใจแค่ "middleware คือฟังก์ชันคั่นกลางที่เรียก next()"
// พอแล้ว ยังไม่ต้องเข้าใจ JWT ทั้งหมดตอนนี้ — ลองนึกภาพง่าย ๆ ว่า middleware ตัวนี้แค่ "เช็คบัตรผ่าน
// ก่อนเข้า ถ้าไม่มีบัตรก็ไม่ให้เข้า (ตอบ 401 แล้วจบ)" กลับมาอ่านรายละเอียด JWT ตอนต้องทำระบบ login จริง
// middleware ตรวจ auth — ถ้าไม่ผ่าน "ตอบเลย" ไม่เรียก next() (จบโซ่)
// 📝 JWT (JSON Web Token) = สตริงที่ server ออกให้ client หลัง login ใช้ "พิสูจน์ตัวตน" ใน request ถัดไป
//    โครงสร้าง: header.payload.signature — payload อ่านได้ (ไม่ใช่ encryption) แต่ signature
//    ถูกเซ็นด้วย secret ของ server ดังนั้น client แก้ payload ไม่ได้ถ้าไม่มี secret
import jwt from "jsonwebtoken";  // npm install jsonwebtoken

function requireAuth(req, res, next) {
  const header = req.headers.authorization;          // "Bearer eyJhbGci..."
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "ต้อง login" });
  }
  const token = header.slice(7);
  try {
    // ⚠️ ตัวอย่างนี้สมมติว่ามี process.env.JWT_SECRET ตั้งไว้แล้ว (ดูวิธีตั้งค่า env ในบทที่ 7)
    //    ถ้าลืมตั้ง JWT_SECRET ตัวแปรจะเป็น undefined — jsonwebtoken จะ throw
    //    "secretOrPrivateKey must have a value" ซึ่ง catch ข้างล่างจับได้และตอบ 401 เหมือนกัน
    //    แต่ 401 นั้นจะทำให้เข้าใจผิดว่า "token ผิด" ทั้งที่จริงคือ **ลืมตั้งค่า secret** — ถ้าเจอ 401
    //    ทุก request ให้เช็ก env var ก่อนสงสัยที่ token
    // ⚠️ ต้อง **verify signature** ของ token ไม่ใช่แค่ decode!
    //    jwt.decode() อ่าน payload เฉย ๆ ไม่เช็ค signature — client ปลอม token ได้ทันที (CVE-grade pitfall)
    //    ⚠️ ต้องระบุ `algorithms` ชัดเจน — ไม่งั้นถูก "alg: none" exploit (= วิธีโจมตีช่องโหว่)
    //    คือ attacker ปลอม token โดยตั้ง alg เป็น "none" แล้วเอา signature ออก หลอกให้ library
    //    เข้าใจว่า "ไม่ต้องเช็ก signature" แล้วเชื่อว่า token ถูกต้อง (CVE-2015-9235 ตำนาน)
    req.user = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ["HS256"],                          // อนุญาตเฉพาะ algorithm ที่ตั้งใจใช้
    });
    next();
  } catch {
    return res.status(401).json({ error: "token ใช้ไม่ได้" });
  }
}

// ใช้ middleware เฉพาะบาง route
app.get("/profile", requireAuth, (req, res) => {
  res.json(req.user);   // มาจาก middleware ข้างบน
});

⚠️ Pitfall อันดับ 1 ของ Express: ลืมเรียก next() ใน middleware → request ค้างจน timeout (ไม่มีอะไรเกิดขึ้น เบราว์เซอร์หมุนติ้ว ๆ) · กลับกัน เรียก next() แล้ว ยัง res.json() ต่อ → error Cannot set headers after they are sent (แปล: "ตั้ง header ไม่ได้แล้ว เพราะส่ง response ไปแล้ว" — เกิดจากตอบ 2 ครั้งใน request เดียว เช่น เรียก res.json() แล้วยัง next() ต่อ หรือ middleware 2 ตัวต่างคนต่าง res.send())

ทำไมเรื่องนี้สำคัญ: NestJS (และ framework ใหญ่ทุกตัว) สร้างบนแนวคิด middleware นี้ — แค่ Nest แบ่งโซ่ออกเป็นชั้นที่มีชื่อชัด (guard, interceptor, pipe, filter — NestJS บทที่ 3) เมื่อคุณเข้าใจ "request ไหลผ่านโซ่ฟังก์ชัน" ที่นี่ การขึ้น Nest จะง่ายมาก


5. Express → NestJS: ต่อยอดกันยังไง

text
         http (core)          ← จัดการ TCP/HTTP ดิบ ทำเองทุกอย่าง (เจ็บ)
            ↓ ห่อ
         Express              ← routing + middleware + req.body (สะดวกขึ้น แต่ไม่มีโครง)
            ↓ ห่อ + ใส่ opinion
         NestJS               ← เพิ่มโครงสร้าง: module/controller/service + DI + decorator
                                (Express adapter เป็น default — Nest ยืนบน Express)
  • http — เห็นทุกอย่าง แต่ทำเองหมด (บทนี้ข้อ 1–2)
  • Express — ลดงานซ้ำ ให้ routing + middleware แต่ "โครงสร้างไฟล์/DI ทำเอง" (ข้อ 3–4)
  • NestJS — ใส่โครงสร้างมาตรฐาน + DI container + validation + lifecycle pipeline ทับ Express อีกที — เหมาะกับทีมใหญ่/ระบบโต (ทำไมต้องมี Nest อ่านได้ที่นี่)

📝 ศัพท์ NestJS ที่จะเจอบ่อย (อธิบายยาวในบท NestJS):

  • DI (Dependency Injection) = ไม่ new service เอง แต่ "ขอ" ผ่าน constructor แล้ว framework หามาให้ → test ง่าย, สลับ implementation ได้
  • decorator = annotation ขึ้นต้นด้วย @ (เช่น @Controller(), @Get()) บอก Nest ว่า class/method นี้มีหน้าที่อะไร — เป็นวิธีเขียน metadata แทน config file
  • Express adapter = ชั้นที่ทำให้ Nest รันบน Express ได้ (default) — สลับเป็น Fastify ก็ได้

อ่านจบบทนี้แล้ว ประโยคแรกของ NestJS บทที่ 0 — "ปัญหาของ Node + Express แบบเปล่า ๆ คืออะไร" — จะไม่ใช่เรื่องลึกลับอีกต่อไป เพราะคุณเพิ่งเจอความเจ็บนั้นมากับมือ


6. Node เป็น HTTP client — เรียก API อื่นด้วย fetch

backend มักต้องเรียก API อื่น (payment, ส่ง SMS, microservice อื่น) — Node มี fetch built-in ตั้งแต่ v18 (เหมือนเบราว์เซอร์ ไม่ต้องลง axios):

javascript
// GET — เรียก API แล้วแปลงเป็น JSON
const res = await fetch("https://api.example.com/users/1");
if (!res.ok) {                          // ⚠️ fetch ไม่ throw เมื่อ status 4xx/5xx!
  throw new Error(`API ตอบ ${res.status}`);
}
const user = await res.json();

// POST พร้อม body + timeout (บทที่ 3 — AbortSignal)
// 📝 AbortSignal.timeout() = static method มาตั้งแต่ Node 17.3+ (Node 18 LTS ขึ้นไปมีแน่ — ตัวอย่างเราใช้ Node 22 LTS)
const created = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "สมชาย" }),
  signal: AbortSignal.timeout(5000),    // ยกเลิกถ้าเกิน 5 วิ
});
// ⭐ ต้อง consume body เสมอ ไม่งั้น connection ใน pool ค้าง (memory leak ช้า ๆ)
if (created.ok) {
  await created.json();                 // หรือ await created.body.cancel() ถ้าไม่ใช้
}

⚠️ Pitfall ใหญ่ของ fetch: มัน ไม่ throw error เมื่อ HTTP status เป็น 404/500 — มัน resolve ปกติ! ต้องเช็ก res.ok (หรือ res.status) เอง · fetch throw เฉพาะตอน network พังจริง ๆ (เน็ตหลุด, DNS ไม่เจอ, timeout)

🚀 โซนขั้นสูง — ข้ามได้รอบแรก · fetch ใน production — ของเพิ่มที่ตัวอย่างข้างบนไม่ได้ทำ:

  • Keep-alive / connection pooling (= เก็บ TCP connection ไว้ใช้ซ้ำแทนเปิด-ปิดใหม่ทุกครั้ง) — fetch ใน Node ใช้ undici ข้างใต้ (ไลบรารี HTTP client ที่ Node ใช้ทำ fetch) · ถ้าเรียก API เดิมบ่อย ๆ (RPS สูง) ใช้ undici.Agent หรือ Pool แชร์ TCP connection แทนเปิดใหม่ทุกครั้ง
  • Retry policy — transient 5xx (502/503/504 = error ชั่วคราวที่ retry แล้วอาจสำเร็จ) ควร retry แบบ exponential backoff (= เว้นเวลารอเพิ่มขึ้นเรื่อย ๆ ทุกครั้งที่ retry เช่น 1s → 2s → 4s) อย่าใช้ retry แบบ fixed interval (รอเท่ากันทุกครั้ง) — จะเกิด thundering herd (= ทุก client แห่ retry พร้อมกันตอนเดียวจนถล่มระบบที่เพิ่งฟื้นซ้ำอีกรอบ) ตอน downstream ฟื้น
  • HTTPS — production URL ควรเป็น https:// เสมอ · Node เป็น HTTP server ภายในมักฟัง http:// แล้วให้ reverse proxy (nginx, ALB, Cloudflare) ทำ TLS termination (= จุดที่ถอดรหัส HTTPS ออกเป็น HTTP ธรรมดาก่อนส่งต่อเข้า server ภายใน) ข้างหน้า · อย่าใส่ certificate ใน Node ตรง ๆ ถ้ามี reverse proxy อยู่แล้ว (จัดการ cert ที่เดียวง่ายกว่า)

🛠️ Checkpoint 5 — ลงมือก่อนไปบทถัดไป

  1. สร้าง server จาก http เปล่า ตอบ { "status": "ok" } ที่ GET /health และ 404 ที่ path อื่น (ตอบ JSON เอง)
  2. ใน server ข้อ 1 เพิ่ม POST /echo ที่อ่าน body (loop for await บน req) แล้วตอบ body กลับ — รู้สึกถึงงานที่ต้องทำเอง
  3. เขียนใหม่ทั้งหมดด้วย Express — เทียบจำนวนบรรทัดและความชัดเจน
  4. เพิ่ม Express middleware ที่ log ทุก request (method + url + เวลาที่ใช้ตอบเป็น ms) — ใบ้: เก็บ Date.now() ตอนเข้า แล้วลบใน res.on("finish", ...)
  5. เพิ่ม route GET /users/:id + middleware requireAuth ที่เช็ก header x-api-key — ถ้าไม่มี ตอบ 401
  6. ใช้ fetch ดึง https://jsonplaceholder.typicode.com/todos/1 แล้วพิมพ์ผล — ลองเปลี่ยน URL เป็น path ที่ได้ 404 แล้วสังเกตว่า fetch ไม่ throw (ต้องเช็ก res.ok)

เฉลยข้อ 1 (ส่วน routing):

javascript
if (req.method === "GET" && req.url === "/health") {
  res.setHeader("Content-Type", "application/json");
  return res.end(JSON.stringify({ status: "ok" }));
}
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));

สรุปบทที่ 5

  • core http: createServer((req, res) => ...)req เป็น Readable stream, res เป็น Writable stream
  • เขียน server เปล่า ๆ ต้องทำเองหมด: routing, อ่าน body จาก stream, parse JSON, ตอบ JSON, 404 — นี่คือเหตุผลที่ framework เกิด
  • Express ห่อ http: ให้ app.get/post(path, handler), req.params, req.body (ผ่าน express.json()), res.json()
  • middleware = ฟังก์ชันคั่นกลางที่ต่อเป็นโซ่ — เรียก next() เพื่อส่งต่อ หรือ res.json() เพื่อจบ · ลืม next() = request ค้าง
  • ลำดับชั้น: http → Express → NestJS — Nest ใส่โครงสร้าง + DI ทับ Express
  • Node เป็น client ด้วย fetch built-in — แต่ ต้องเช็ก res.ok เอง (fetch ไม่ throw ที่ 4xx/5xx)

บทต่อไป: error handling ระดับ production (request handler ที่ throw จะทำให้ server ตายไหม?) + วิธี debug Node อย่างมือโปรด้วย --inspect

→ บทที่ 6: Error Handling + Debugging


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03