Skip to content

HARRY-3 — GĐ2 — Nền tảng: HTTP / Node runtime / async / networking

GĐ2 — Nền tảng: HTTP / Node runtime / async / networking

Study note cho FE engineer (mạnh JS/TS) chuyển sang Backend. Mỗi concept: định nghĩatại sao quan trọngcơ chếví dụpitfall. Thuật ngữ giữ tiếng Anh.


Phần A — HTTP protocol

1. HTTP method semantics & idempotency

Định nghĩa ngắn. HTTP method là "động từ" mô tả ý định thao tác lên một resource: GET (đọc), POST (tạo/hành động), PUT (thay thế toàn bộ), PATCH (sửa một phần), DELETE (xoá).

  • safe = không thay đổi state trên server (chỉ đọc). GET, HEAD là safe.
  • idempotent = gọi 1 lần hay N lần (cùng payload) cho ra cùng một server state cuối. GET, PUT, DELETE idempotent. POST không idempotent. PATCH thường không đảm bảo.

Tại sao quan trọng với backend. Backend sống trong môi trường có retry: client timeout rồi gửi lại, load balancer replay, mobile mạng chập chờn. Nếu endpoint idempotent thì retry an toàn — không tạo 2 order, không trừ tiền 2 lần. Method semantics còn quyết định cache (chỉ GET được cache), routing, và cả việc proxy có được phép tự động retry hay không.

Cơ chế / cách hoạt động.

  • GET: params ở URL/query, không body semantic. Được cache, được prefetch. → tuyệt đối không được có side-effect.
  • POST: tạo resource mới (server sinh id) HOẶC trigger một action (gửi email, thanh toán). Mỗi lần gọi = một tác động mới.
  • PUT /users/42: client biết trước id, gửi toàn bộ representation. Gọi lại → ghi đè cùng nội dung → state không đổi thêm ⇒ idempotent.
  • PATCH /users/42: gửi delta ({ "email": "x" }). Idempotent nếu delta là "set field = value"; KHÔNG idempotent nếu delta kiểu { "$inc": { "credits": 5 } }.
  • DELETE /users/42: xoá lần 1 → 200/204; lần 2 → 404 (status khác nhau nhưng state vẫn là "đã xoá") ⇒ vẫn idempotent.

Ví dụ thực tế — idempotency key cho POST.

ts
// POST /payments không idempotent → dùng key để chống double-charge khi client retry
app.post("/payments", async (req, res) => {
  const key = req.header("Idempotency-Key"); // client sinh UUID, giữ nguyên khi retry
  const existing = await db.findPayment(key);
  if (existing) return res.status(200).json(existing); // trả kết quả cũ, không charge lại
  const p = await charge(req.body);
  await db.savePayment(key, p);
  res.status(201).json(p);
});

Pitfall hay gặp.

  • Dùng GET cho action có side-effect (vd GET /users/42/delete) → crawler/browser prefetch quét sạch data. Kinh điển.
  • Nghĩ "PUT với PATCH giống nhau, dùng cái nào cũng được". PUT thiếu field → nhiều framework hiểu là "set field đó về null" → mất dữ liệu.
  • Cho phép LB tự retry POST non-idempotent → double order. Fix: idempotency key hoặc chỉ retry method idempotent.

2. HTTP status codes đúng chuẩn

Định nghĩa ngắn. Số 3 chữ số báo kết quả: 2xx thành công, 3xx redirect, 4xx lỗi phía client, 5xx lỗi phía server.

Tại sao quan trọng. Status code là contract máy-đọc-được: client, LB, monitoring, retry logic đều dựa vào nó. Trả sai (vd luôn 200 kèm {error:...}) khiến alert không kêu, client không biết retry hay không, cache hỏng.

Cơ chế — chọn cái nào.

Success:

  • 200 OK — thành công, có body trả về (GET, hoặc POST/PUT trả entity).
  • 201 Created — tạo resource mới; nên kèm header Location: /users/42.
  • 204 No Content — thành công nhưng không có body (DELETE, hoặc PUT chỉ cần xác nhận). Client không được parse body.

Client error (4xx — lỗi do request, đừng retry y nguyên):

  • 400 Bad Request — request malformed: JSON parse fail, thiếu field bắt buộc, sai kiểu ở tầng syntax.
  • 422 Unprocessable Entity — syntax OK nhưng vi phạm business/validation rule (email đúng format nhưng đã tồn tại, tuổi = -5). Nhiều team gộp hết vào 400; tách 422 rõ ràng hơn cho validation.
  • 401 Unauthorizedchưa xác thực / token sai/hết hạn ("anh là ai?"). → client nên login lại.
  • 403 Forbidden — đã biết anh là ai nhưng không đủ quyền ("biết rồi, nhưng cấm"). → login lại vô ích.
  • 404 Not Found — resource không tồn tại (hoặc cố tình giấu sự tồn tại vì lý do security).
  • 409 Conflict — xung đột state: tạo trùng unique, optimistic-lock version mismatch, xoá cái đang được reference.
  • 429 Too Many Requests — rate limit; nên kèm header Retry-After.

Server error (5xx — lỗi phía server, thường có thể retry):

  • 500 Internal Server Error — bug/exception không lường trước trong app của mình.
  • 502 Bad Gateway — mình là proxy/gateway, upstream trả response hỏng.
  • 503 Service Unavailable — server tạm quá tải / đang deploy / maintenance; kèm Retry-After.
  • 504 Gateway Timeout — proxy chờ upstream quá lâu, hết giờ.

Ví dụ.

ts
if (!body.email) return res.status(400).json({ error: "email required" });      // syntax
if (!isEmail(body.email)) return res.status(422).json({ error: "email invalid" }); // semantic
if (await db.exists(body.email)) return res.status(409).json({ error: "taken" }); // conflict

Pitfall.

  • 200 với error body — "always 200" là anti-pattern lớn nhất: monitoring thấy 100% success trong khi user fail hết.
  • Nhầm 401/403: trả 403 khi token hết hạn → client không tự refresh token.
  • Trả 500 cho lỗi validation → client tưởng server sập và retry, làm nặng thêm.
  • Trả 404 khi thực ra là 403 đôi khi cố ý (không lộ resource tồn tại) — biết để không nhầm là bug.

3. HTTP headers quan trọng

Định nghĩa ngắn. Header là các cặp key-value metadata đi kèm request/response, mô tả nội dung, xác thực, cache, độ dài body.

Tại sao quan trọng. Backend đọc header để biết parse body kiểu gì, ai đang gọi, có được trả cache không; và set header để điều khiển cache/CDN, bảo mật, streaming.

Cơ chế.

  • Content-Type — kiểu body đang gửi: application/json, application/x-www-form-urlencoded, multipart/form-data, text/plain. Server chọn parser theo header này. Sai Content-Type → parse fail.
  • Accept — client nói "tôi muốn nhận định dạng gì" (application/json). Server dùng cho content negotiation.
  • Authorization — credential: thường Bearer <jwt> hoặc Basic <base64>. Đây là nơi token nằm, không phải cookie (trừ session-cookie flow).
  • Cache-Control — chỉ thị cache: no-store (không lưu), no-cache (lưu nhưng phải revalidate), max-age=60, private/public.
  • ETag — "fingerprint" của một version resource. Client gửi lại If-None-Match: <etag>; nếu chưa đổi server trả 304 Not Modified (không body) → tiết kiệm băng thông.
  • Content-Length vs Transfer-Encoding: chunked — cách báo độ dài body. Content-Length = biết trước tổng bytes (buffer sẵn). chunked = stream, gửi từng chunk, không cần biết tổng size trước (dùng khi tạo dữ liệu on-the-fly). Không dùng đồng thời cả hai.

Ví dụ — ETag revalidation.

ts
const etag = hash(user);                 // "W/\"abc123\""
if (req.header("if-none-match") === etag) return res.status(304).end();
res.setHeader("ETag", etag).json(user);

Pitfall.

  • Quên set Content-Type: application/json khi trả JSON → client/browser hiểu nhầm là text, không parse.
  • Cache dữ liệu per-user với Cache-Control: public → CDN phục vụ data của user A cho user B (data leak nghiêm trọng). Data cá nhân phải private + no-store.
  • Set cả Content-Length sai lệch so với body thực → response bị cắt cụt hoặc treo.

4. CORS phía server

Định nghĩa ngắn. CORS (Cross-Origin Resource Sharing) là cơ chế của browser: một trang ở origin A gọi API ở origin B (khác scheme/host/port) sẽ bị chặn đọc response trừ khi server B chủ động cho phép qua các header Access-Control-*.

Tại sao quan trọng. FE hay báo "bị CORS chặn" — nhưng CORS được fix ở server, không phải ở FE. BE dev phải hiểu để cấu hình đúng, không tắt bừa (*) gây lỗ hổng.

Cơ chế.

  • Với request "đơn giản" (GET/POST đơn giản), browser gửi thẳng nhưng chỉ cho JS đọc response nếu Access-Control-Allow-Origin khớp origin.
  • Với request "phức tạp" (method PUT/DELETE, có Authorization hay Content-Type: application/json), browser gửi preflight OPTIONS trước để hỏi server "tôi được phép gọi không?". Server phải trả:
    • Access-Control-Allow-Origin: https://app.example.com (hoặc *)
    • Access-Control-Allow-Methods: GET,POST,PUT,DELETE
    • Access-Control-Allow-Headers: Content-Type,Authorization
    • Access-Control-Allow-Credentials: true (nếu gửi cookie/credential)
  • Chỉ khi preflight OK, browser mới gửi request thật.

Ví dụ — set CORS bằng http thuần.

ts
res.setHeader("Access-Control-Allow-Origin", "https://app.example.com");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
if (req.method === "OPTIONS") return res.writeHead(204).end(); // trả preflight sớm

Pitfall.

  • Allow-Origin: * cùng Allow-Credentials: true — browser từ chối, và về bảo mật là sai. Với credentials phải echo lại đúng origin cụ thể (không được *).
  • Server không handle method OPTIONS (route chỉ có GET/POST) → preflight nhận 404/405 → FE thấy "CORS error" dù logic đúng.
  • Nghĩ CORS là bảo mật server → không. CORS chỉ bảo vệ browser user; curl/backend vẫn gọi được thoải mái. Auth vẫn phải làm riêng.

Phần B — Node.js runtime & async

5. Node.js event loop

Định nghĩa ngắn. Event loop là vòng lặp đơn luồng (single main thread) của Node, liên tục lấy callback đã "chín" ra chạy. Nó cho phép xử lý nhiều kết nối đồng thời mà chỉ 1 thread JS.

Tại sao quan trọng. Một Node process = một thread chạy JS. Hiểu event loop = hiểu vì sao một đoạn tính toán nặng đồng bộ có thể làm treo toàn bộ server (mọi request khác chờ), và vì sao thứ tự setTimeout vs Promise không như trực giác.

Cơ chế — các phase (một tick):

  1. timers — chạy callback của setTimeout/setInterval đã tới hạn.
  2. pending callbacks — một số I/O callback bị hoãn.
  3. poll — chờ và xử lý I/O mới (đọc file, socket). Phase Node "nằm chờ" ở đây.
  4. check — chạy setImmediate.
  5. close — callback close (vd socket đóng).

Microtask queue (Promise .then, queueMicrotask, process.nextTick) không phải một phase — nó được flush sạch sau mỗi callbackgiữa các phase. Nghĩa là microtask luôn ưu tiên hơn macrotask kế tiếp.

  • macrotask: setTimeout, setImmediate, I/O callback.
  • microtask: Promise.then, await (phần sau await), queueMicrotask, process.nextTick (còn ưu tiên hơn cả Promise microtask).

Ví dụ — thứ tự in ra.

ts
console.log("1");
setTimeout(() => console.log("2-timeout"), 0); // macrotask
Promise.resolve().then(() => console.log("3-promise")); // microtask
console.log("4");
// Output: 1, 4, 3-promise, 2-timeout
// sync trước → microtask flush hết → mới tới macrotask

Pitfall.

  • CPU nặng đồng bộ block cả server: for loop 5 tỷ vòng hoặc JSON.parse payload 50MB chặn event loop → mọi request khác đứng hình, health check fail, LB tưởng server chết.
ts
app.get("/heavy", (req, res) => {
  let s = 0; for (let i = 0; i < 5e9; i++) s += i; // chặn ~vài giây, toàn bộ server treo
  res.json({ s });
});
  • Lạm dụng process.nextTick đệ quy → microtask queue không bao giờ cạn → starve I/O (poll phase không tới lượt) → server "sống" nhưng không nhận request mới.

6. libuv & thread pool

Định nghĩa ngắn. libuv là thư viện C dưới Node lo phần async I/O + cung cấp một thread pool (mặc định 4 thread) để chạy các tác vụ không có async OS-native.

Tại sao quan trọng. "Node single-threaded" chỉ đúng cho JS. I/O thật ra chạy song song nhờ libuv. Hiểu điều này để biết cái gì thực sự song song, cái gì vẫn nghẽn, và tune UV_THREADPOOL_SIZE.

Cơ chế.

  • Network I/O (TCP/HTTP socket) dùng cơ chế OS non-blocking (epoll/kqueue/IOCP) → không tốn thread pool, scale rất tốt.

  • Một số tác vụ không có async OS API phải chạy trên thread pool: fs.* (file), crypto.pbkdf2/bcrypt, zlib (nén), DNS lookup. Pool mặc định = 4 → chỉ 4 tác vụ loại này chạy song song, phần còn lại xếp hàng.

  • UV_THREADPOOL_SIZE (env, đặt trước khi Node khởi động) tăng số thread pool.

  • I/O-bound: phần lớn thời gian chờ (DB, network, disk) → Node xử lý cực tốt, nhiều nghìn connection.

  • CPU-bound: phần lớn thời gian tính (hash, resize ảnh, ML) → chặn thread → cần worker_threads/tách service.

Ví dụ.

bash
UV_THREADPOOL_SIZE=16 node server.js   # nhiều thao tác bcrypt/fs song song hơn

Pitfall.

  • Login endpoint hash bcrypt (CPU + dùng thread pool). 4 thread → chỉ 4 login đồng thời, request thứ 5 chờ, mà event loop vẫn có vẻ rảnh → latency tăng khó hiểu. Fix: tăng pool size / offload.
  • Tưởng tăng UV_THREADPOOL_SIZE giúp mọi thứ nhanh → không giúp network I/O (vốn không dùng pool), và nếu > số CPU core thì tranh CPU.

7. Blocking vs non-blocking

Định nghĩa ngắn. Blocking call giữ thread lại chờ kết quả trước khi làm gì khác; non-blocking trả về ngay và báo kết quả qua callback/Promise sau.

Tại sao quan trọng. Trên một event loop dùng chung, một call blocking = tất cả request đóng băng trong thời gian đó. Đây là lỗi FE-chuyển-sang-BE hay mắc nhất.

Cơ chế.

  • fs.readFileSync(path) — chặn event loop tới khi đọc xong file → không request nào khác được phục vụ.
  • await fs.promises.readFile(path) — non-blocking; libuv đọc trên thread pool, event loop tiếp tục phục vụ request khác, callback resume khi xong.

Ví dụ — 1 vòng lặp/1 sync call làm treo request khác.

ts
import { readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";

// ❌ blocking: mọi request khác chờ đọc file xong
app.get("/bad", (req, res) => res.send(readFileSync("./big.json", "utf8")));

// ✅ non-blocking: các request khác vẫn chạy trong lúc I/O
app.get("/good", async (req, res) => res.send(await readFile("./big.json", "utf8")));

Pitfall.

  • Dùng *Sync "cho nhanh, cho tiện" trong request handler — chạy local 1 user thấy ổn, lên prod tải cao thì throughput sụp.
  • JSON.parse/JSON.stringify payload khổng lồ là blocking đồng bộ (không có bản async) → giới hạn size body, hoặc stream parse.
  • *Sync chỉ nên dùng ở startup (load config một lần), không dùng trong hot path.

8. Buffer & Stream

Định nghĩa ngắn. Buffer là vùng bộ nhớ nhị phân cố định (mảng byte) — cách Node biểu diễn binary data. Stream là abstraction xử lý dữ liệu theo từng khối tuần tự thay vì nạp hết vào RAM.

Tại sao quan trọng. Backend xử lý file lớn, upload, response khổng lồ, proxy. Đọc nguyên file 2GB vào RAM (Buffer) = OOM crash. Stream cho phép xử lý dữ liệu lớn với RAM nhỏ và trả bytes cho client ngay khi có.

Cơ chế.

  • Buffer: dữ liệu nhị phân trọn vẹn trong RAM. Dùng cho dữ liệu nhỏ/vừa (đọc file config, encode/decode).
  • Stream: Readable (nguồn), Writable (đích), Transform (biến đổi giữa chừng, vd gzip). Dữ liệu chảy theo chunk.
  • Backpressure: khi đích ghi chậm hơn nguồn đọc, stream báo "chậm lại" để RAM không phình. pipe() xử lý backpressure tự động; nếu tự viết .on("data") phải tự quản.
  • pipe() nối Readable → Writable và lo backpressure + kết thúc.

Ví dụ — stream file thay vì nạp hết.

ts
import { createReadStream } from "node:fs";
// ✅ chảy từng chunk, RAM thấp, client nhận sớm, tự lo backpressure
createReadStream("./video.mp4").pipe(res);

// ❌ nạp cả file vào RAM rồi mới gửi → OOM với file lớn, nhiều user
// res.end(await readFile("./video.mp4"));

Pitfall.

  • Quên backpressure khi tự làm (readable.on("data", d => writable.write(d))) — nếu write() trả false mà vẫn đọc tiếp → buffer nội bộ phình → RAM nổ. Dùng pipe/pipeline.
  • Không handle error trên stream → uncaught exception làm crash process. Dùng stream.pipeline(src, dst, cb) để bắt lỗi mọi mắt xích.
  • Buffer + string encoding: cắt Buffer giữa một ký tự multi-byte (UTF-8) rồi toString → ký tự vỡ. Dùng StringDecoder khi cần.

9. process.env & config, NODE_ENV

Định nghĩa ngắn. process.env là object chứa environment variables — cách chuẩn để đưa config (secret, URL DB, port) vào app từ bên ngoài code.

Tại sao quan trọng. 12-factor: config tách khỏi code. Cùng một artifact chạy dev/staging/prod chỉ khác env. Secret (DB password, API key) không được hardcode/commit — phải qua env.

Cơ chế.

  • Mọi giá trị process.env.Xstring (hoặc undefined). Phải tự parse số/boolean.
  • NODE_ENV quy ước: development | production | test. Nhiều lib (Express, React) bật tối ưu/tắt log chi tiết khi production.
  • Load từ file .env (vd package dotenv) ở dev; prod thường inject qua orchestrator (Docker/K8s/PaaS).

Ví dụ — validate config sớm khi boot.

ts
const PORT = Number(process.env.PORT ?? 3000);       // env là string → ép Number
const isProd = process.env.NODE_ENV === "production";
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL missing"); // fail fast

Pitfall.

  • process.env.PORT === 3000 → luôn false (string vs number).
  • Quên set NODE_ENV=production trên prod → chạy chậm, log verbose, lộ stack trace.
  • Commit .env chứa secret lên git → rò rỉ. .env phải nằm trong .gitignore.
  • Config đọc rải rác process.env.X khắp code → nên gom vào một module config.ts validate một lần.

10. worker_threads vs cluster vs child_process

Định nghĩa ngắn. Ba cách chạy code ngoài main thread/process:

  • worker_threads — nhiều thread JS trong cùng process, share memory được (SharedArrayBuffer).
  • cluster — fork nhiều process Node giống nhau, cùng listen 1 port (LB nội bộ) để dùng nhiều CPU core.
  • child_process — spawn process khác (Node hoặc chương trình bất kỳ: ffmpeg, python) và giao tiếp qua stdio/IPC.

Tại sao quan trọng. Node 1 thread JS → không tự dùng hết nhiều core, và CPU-bound làm nghẽn event loop. Ba công cụ này giải bài toán "scale multi-core" và "offload CPU nặng".

Cơ chế / khi nào dùng.

  • CPU-bound trong app (hash, parse lớn, image/video, tính toán): worker_threads. Đẩy việc nặng sang worker để main thread rảnh phục vụ request.
  • Tận dụng nhiều core cho throughput HTTP: cluster (hoặc chạy N instance sau reverse proxy / PM2 / K8s). Mỗi process 1 event loop độc lập.
  • Chạy chương trình ngoài / cô lập crash: child_process (spawn cho output stream lớn, exec cho lệnh ngắn). Vd gọi ffmpeg.

Ví dụ — offload CPU sang worker.

ts
import { Worker } from "node:worker_threads";
app.get("/report", (req, res) => {
  const w = new Worker("./heavy-worker.js", { workerData: req.query });
  w.on("message", (out) => res.json(out));  // main thread không bị chặn
  w.on("error", (e) => res.status(500).json({ error: String(e) }));
});

Pitfall.

  • Dùng cluster để giải CPU-bound trong 1 request → không giúp: request nặng vẫn chặn cả 1 worker-process của nó. cluster chỉ tăng số request song song, không tăng tốc 1 request.
  • Tạo new Worker mỗi request → chi phí spawn cao → nên dùng worker pool (piscina).
  • child_process với input là user data không sanitize → command injection. Dùng spawn với mảng args (không qua shell), tránh exec(\cmd ${userInput}`)`.

11. Async mastery

Định nghĩa ngắn. Điều phối nhiều Promise và xử lý lỗi async đúng cách: chạy song song vs tuần tự, gộp kết quả, và bắt mọi rejection.

Tại sao quan trọng. Backend thường gọi nhiều I/O (DB + cache + service ngoài). Làm tuần tự khi có thể song song = latency cộng dồn. Bỏ lọt một rejection = crash process hoặc request treo.

Cơ chế — các combinator:

  • Promise.all([...]) — chạy song song, chờ tất cả; fail-fast: một cái reject → cả all reject ngay (các cái khác vẫn chạy tiếp nhưng kết quả bị bỏ).
  • Promise.allSettled([...]) — chờ tất cả xong, trả mảng {status, value|reason}không fail-fast; dùng khi muốn biết cái nào ok/lỗi.
  • Promise.race([...]) — resolve/reject theo cái xong đầu tiên (kể cả lỗi). Dùng cho timeout.
  • Promise.any([...]) — resolve theo cái thành công đầu tiên; chỉ reject (AggregateError) khi tất cả fail. Dùng cho "thử nhiều nguồn, lấy cái nào nhanh".

Error propagation trong async/await. await "unwrap" Promise; nếu Promise reject, await throw → bắt bằng try/catch. Lỗi lan lên caller như exception đồng bộ.

Ví dụ — sequential vs parallel & timeout.

ts
// ❌ tuần tự: tổng thời gian = a + b + c
const u = await getUser(id);
const o = await getOrders(id);
const p = await getPrefs(id);

// ✅ song song: tổng thời gian = max(a,b,c)
const [u2, o2, p2] = await Promise.all([getUser(id), getOrders(id), getPrefs(id)]);

// timeout bằng race
const withTimeout = <T>(pr: Promise<T>, ms: number) =>
  Promise.race([pr, new Promise<never>((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);

Pitfall.

  • Sequential await pitfall: await trong for khi các vòng độc lập → cộng dồn latency. Dùng Promise.all(items.map(...)) (nhưng coi chừng bắn 10k request song song → cần giới hạn concurrency).
  • Quên await (floating promise): db.save(x) không await → lỗi thành unhandledRejection, và code chạy tiếp trước khi save xong.
  • unhandledRejection: Promise reject không ai .catch → Node (mặc định) sẽ crash process. Luôn try/catch quanh await, hoặc .catch cho fire-and-forget; và đặt handler cuối cùng:
ts
process.on("unhandledRejection", (e) => { logger.error(e); /* rồi graceful shutdown */ });
  • Promise.all fail-fast làm 1 lỗi nhỏ giết cả batch — khi muốn "cố hết sức" dùng allSettled.

Phần C — Networking cơ bản

12. DNS, TCP, TLS, port, reverse proxy

Định nghĩa ngắn. Chuỗi bước để một request HTTP thực sự tới được server: phân giải tên miền → bắt tay TCP → bắt tay TLS (HTTPS) → truyền dữ liệu; và lớp reverse proxy đứng trước app.

Tại sao quan trọng. Latency, timeout, "connection refused", chứng chỉ hết hạn, cấu hình proxy — tất cả bug này BE phải debug. Hiểu chuỗi để biết lỗi nằm ở tầng nào.

Cơ chế.

  • DNS resolve: đổi api.example.com → IP (vd 52.x.x.x). Có cache (TTL) nhiều tầng: OS, resolver, ISP. Chậm/hỏng DNS = request "đứng" trước cả khi kết nối.
  • Port: một IP có 65535 port; server "listen" trên một port (HTTP 80, HTTPS 443, app dev 3000). IP:port xác định đúng process.
  • TCP 3-way handshake: SYN (client) → SYN-ACK (server) → ACK (client). Sau đó mới có kênh tin cậy, có thứ tự. Tốn 1 round-trip trước khi gửi data.
  • TLS handshake (HTTPS, tóm tắt): sau TCP, client/server trao đổi để (1) xác thực server qua certificate, (2) thỏa thuận cipher, (3) sinh session key đối xứng. Từ đó dữ liệu được mã hoá. Tốn thêm round-trip(s) (TLS 1.3 nhanh hơn, 1-RTT).
  • Reverse proxy (Nginx): đứng trước app, nhận request từ internet rồi forward vào backend. Vai trò: TLS termination (giải mã HTTPS 1 chỗ), load balancing nhiều instance, phục vụ static file, rate limit, caching, che cấu trúc nội bộ, buffering request chậm.

Ví dụ — sơ đồ đường đi.

Browser
  → DNS: api.example.com → 52.10.1.5
  → TCP handshake tới 52.10.1.5:443 (SYN/SYN-ACK/ACK)
  → TLS handshake (verify cert, agree keys)
  → HTTP request
  → [Nginx :443] TLS termination + LB  →  Node app :3000 (cluster/instances)

Pitfall.

  • App Node listen 127.0.0.1 thay vì 0.0.0.0 trong container → Nginx/host không tới được → "connection refused".
  • Quên rằng reverse proxy che IP thật của client → phải đọc X-Forwarded-For (và cấu hình trust proxy) nếu cần IP/log/rate-limit đúng.
  • Certificate hết hạn / thiếu intermediate chain → browser báo lỗi TLS dù server "chạy".
  • DNS TTL cao + đổi IP → client vẫn gọi IP cũ một thời gian.
  • Không đặt timeout cho outbound call (gọi service ngoài) → DNS/TCP treo kéo cả request pool cạn kiệt.

Thực hành & Done khi

Bài thực hành — HTTP server thuần bằng http module

Yêu cầu: routing tay, parse JSON body, trả JSON đúng status/header. Không dùng framework.

ts
import { createServer, IncomingMessage } from "node:http";

// đọc & parse body (stream các chunk → gộp → JSON.parse)
function readJson(req: IncomingMessage): Promise<any> {
  return new Promise((resolve, reject) => {
    let raw = "";
    req.on("data", (c) => {
      raw += c;
      if (raw.length > 1e6) reject(new Error("payload too large")); // chặn body khổng lồ
    });
    req.on("end", () => {
      if (!raw) return resolve({});
      try { resolve(JSON.parse(raw)); } catch { reject(new Error("invalid json")); }
    });
    req.on("error", reject);
  });
}

const users: { id: number; name: string }[] = [];
let nextId = 1;

const server = createServer(async (req, res) => {
  const json = (code: number, body: unknown) => {
    res.writeHead(code, { "Content-Type": "application/json" });
    res.end(JSON.stringify(body));
  };
  try {
    const { method = "GET", url = "/" } = req;

    if (method === "GET" && url === "/users") return json(200, users);

    if (method === "POST" && url === "/users") {
      const body = await readJson(req);
      if (!body.name) return json(400, { error: "name required" });   // validation
      const u = { id: nextId++, name: String(body.name) };
      users.push(u);
      res.setHeader("Location", `/users/${u.id}`);
      return json(201, u);                                            // 201 Created
    }

    const m = url.match(/^\/users\/(\d+)$/);
    if (m && method === "GET") {
      const u = users.find((x) => x.id === Number(m[1]));
      return u ? json(200, u) : json(404, { error: "not found" });    // 404
    }
    if (m && method === "DELETE") {
      const i = users.findIndex((x) => x.id === Number(m[1]));
      if (i === -1) return json(404, { error: "not found" });
      users.splice(i, 1);
      res.writeHead(204).end();                                       // 204 No Content
      return;
    }

    return json(404, { error: "route not found" });
  } catch (e) {
    const msg = (e as Error).message;
    return json(msg === "invalid json" ? 400 : 500, { error: msg });  // 400 vs 500
  }
});

server.listen(Number(process.env.PORT ?? 3000), "0.0.0.0");

Test nhanh:

bash
curl -s localhost:3000/users
curl -s -XPOST localhost:3000/users -H 'Content-Type: application/json' -d '{"name":"An"}'
curl -s -XDELETE localhost:3000/users/1 -i   # thấy 204

Tiêu chí hoàn thành GĐ2 (Done khi bạn tự tin làm được)

  • [ ] Chọn đúng method + status code cho mọi case CRUD (200/201/204/400/401/403/404/409/422/429/500) và giải thích được vì sao.
  • [ ] Giải thích idempotency và thiết kế được POST an toàn với retry (idempotency key).
  • [ ] Set/đọc đúng các header cốt lõi; hiểu ETag/304 và chunked vs Content-Length.
  • [ ] Tự cấu hình CORS cho một FE origin, handle preflight OPTIONS, biết cạm bẫy * + credentials.
  • [ ] Vẽ được event loop (phase + micro/macrotask), dự đoán đúng thứ tự setTimeout vs Promise, và giải thích vì sao CPU-bound treo server.
  • [ ] Phân biệt I/O-bound vs CPU-bound, biết vai trò libuv/thread pool và khi nào tune UV_THREADPOOL_SIZE.
  • [ ] Không bao giờ dùng *Sync/vòng lặp nặng trong request handler; biết offload bằng worker_threads.
  • [ ] Dùng stream + pipe cho file lớn, hiểu backpressure.
  • [ ] Quản config qua process.env + validate khi boot; không commit secret; set NODE_ENV.
  • [ ] Thành thạo Promise.all/allSettled/race/any, tránh sequential-await pitfall, bắt unhandledRejection.
  • [ ] Mô tả được đường đi DNS → TCP → TLS → HTTP và vai trò reverse proxy (Nginx).
  • [ ] Viết được HTTP server thuần (routing + parse body + JSON + status) không cần framework.

Câu hỏi mở / chưa giải quyết

  • Chuẩn chọn 400 vs 422 khác nhau theo team/framework — thống nhất convention nội bộ trước khi code.
  • Concurrency limiting (khi Promise.all map hàng nghìn item) sẽ đào sâu ở GĐ sau (p-limit / queue / batching).
  • Graceful shutdown (drain connection, đóng DB pool) đề cập thoáng — nên thành mục riêng ở GĐ deployment.

Học bằng cách build. Chứng minh, đừng tin.