jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Node.js Super Senior · Phase 2 — HTTP & Web Fundamentals

Phase 2: HTTP in depth (methods, status, headers, REST, idempotency), HTTP/1.1 vs HTTP/2, a raw Node server, request parsing, a hand-rolled router, streaming and SSE, cookies, CORS, caching/ETag, compression, and graceful shutdown.

This is Phase 2 of the 10-phase Super Senior path {Đây là Phase 2 của lộ trình Super Senior 10 phase}. Before any framework, a senior understands the protocol underneath {Trước mọi framework, senior hiểu giao thức bên dưới}. We’ll build a real server with only the core http module, discover why middleware exists, and cover the production concerns frameworks hide from you: cookies, CORS, caching, compression, streaming, timeouts, and graceful shutdown {Ta sẽ dựng server thật chỉ bằng core http, khám phá vì sao middleware tồn tại, và bao quát những mối lo production mà framework giấu bạn: cookie, CORS, cache, nén, streaming, timeout, và tắt êm}.

Everything from Phase 1 applies here — an HTTP server is just streams in and streams out {Mọi thứ ở Phase 1 áp dụng — HTTP server chỉ là stream vào và stream ra}.


1. The HTTP protocol {Giao thức HTTP}

Every web interaction is a request and a response, both plain text with a defined structure {Mọi tương tác web là một request và một response, đều là text với cấu trúc xác định}.

REQUEST                          RESPONSE
POST /api/users HTTP/1.1         HTTP/1.1 201 Created
Host: example.com                Content-Type: application/json
Content-Type: application/json   Content-Length: 38
Authorization: Bearer abc
                                 {"id":1,"name":"Ann"}
{"name":"Ann"}
└ method  └ path  └ version      └ status code  └ reason

A message has three parts {Một thông điệp có ba phần}: the start line (method + path + version, or version + status), the headers (metadata), a blank line, then the optional body {dòng đầu, các header (metadata), một dòng trống, rồi body tùy chọn}.

Methods — and the safe/idempotent distinction {Method — và phân biệt safe/idempotent}

MethodPurpose {Mục đích}Safe {An toàn}Idempotent
GETread {đọc}yesyes
HEADread headers only {chỉ header}yesyes
POSTcreate {tạo}nono
PUTreplace fully {thay toàn bộ}noyes
PATCHpartial update {sửa một phần}nono
DELETEremove {xóa}noyes

Safe = no server state change {Safe = không đổi trạng thái server}. Idempotent = doing it twice has the same effect as once {Idempotent = làm hai lần cũng như một lần}. This matters for retries: a client (or proxy) may safely retry an idempotent request after a network blip, but retrying a POST can create duplicates {Điều này quan trọng khi retry: client/proxy có thể an toàn thử lại request idempotent sau sự cố mạng, nhưng thử lại POST có thể tạo bản trùng}.

Senior trick {Mẹo senior}: to make POST safe to retry, accept an idempotency key header and de-duplicate server-side {để POST an toàn khi retry, nhận một header idempotency key và khử trùng phía server}.

Status codes — memorize the families {Status code — nhớ theo nhóm}

2xx Success      200 OK · 201 Created · 202 Accepted · 204 No Content
3xx Redirect     301 Moved Permanently · 302 Found · 304 Not Modified · 307/308 (keep method)
4xx Client error 400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found
                 405 Method Not Allowed · 409 Conflict · 422 Unprocessable · 429 Too Many Requests
5xx Server error 500 Internal · 502 Bad Gateway · 503 Service Unavailable · 504 Gateway Timeout

Senior reflex {Phản xạ senior}: returning the right status code is part of the API contract {trả đúng status code là một phần hợp đồng của API}. A failed create is 400/422, not 200 with an error in the body {Tạo thất bại là 400/422, không phải 200 kèm lỗi trong body}. 401 means not authenticated; 403 means authenticated but not allowed — people confuse these constantly {401chưa xác thực; 403đã xác thực nhưng không được phép — người ta hay nhầm}.

REST principles {Nguyên tắc REST}

  • Resources are nouns in the URL {Tài nguyên là danh từ trong URL}: /users, /users/1/posts — not /getUsers {không phải /getUsers}.
  • The HTTP method is the verb {Method HTTP là động từ}.
  • Stateless — each request carries everything needed; the server keeps no per-client session in memory between requests {Không trạng thái — mỗi request mang đủ thứ cần; server không giữ session từng-client trong bộ nhớ giữa các request}.
  • Use the right status + representation; support content negotiation via Accept {Dùng đúng status + biểu diễn; hỗ trợ thương lượng nội dung qua Accept}.

2. HTTP versions & connection reuse {Phiên bản HTTP & tái dùng kết nối}

A senior knows why an API feels fast or slow at the transport layer {Senior biết vì sao một API nhanh hay chậm ở tầng vận chuyển}.

  • HTTP/1.0 — one request per TCP connection, then close. Expensive: a new TCP (and TLS) handshake every time {một request mỗi kết nối TCP, rồi đóng. Đắt: bắt tay TCP (và TLS) mỗi lần}.
  • HTTP/1.1keep-alive by default: reuse the connection for many sequential requests. But responses on one connection are ordered (head-of-line blocking) {keep-alive mặc định: tái dùng kết nối cho nhiều request tuần tự. Nhưng response trên một kết nối bị xếp thứ tự (nghẽn đầu hàng)}.
  • HTTP/2multiplexing: many concurrent streams over one connection, plus header compression (HPACK) and binary framing — removes most head-of-line blocking at the HTTP layer {ghép kênh: nhiều stream đồng thời trên một kết nối, cộng nén header (HPACK) và đóng khung nhị phân — loại bỏ phần lớn nghẽn đầu hàng ở tầng HTTP}.
  • HTTP/3 — runs over QUIC (UDP), eliminating TCP-level head-of-line blocking; great on lossy networks {chạy trên QUIC (UDP), loại nghẽn đầu hàng ở mức TCP; tốt trên mạng hay mất gói}.
import { createServer } from 'node:http';        // HTTP/1.1
import { createSecureServer } from 'node:http2'; // HTTP/2 (needs TLS in browsers)

In practice you usually run plain HTTP/1.1 in Node and let a reverse proxy (Nginx) or load balancer terminate TLS and speak HTTP/2/3 to the browser (see the Nginx series) {Thực tế bạn thường chạy HTTP/1.1 thuần trong Node và để reverse proxy (Nginx) hoặc load balancer kết thúc TLS và nói HTTP/2/3 với trình duyệt}. Either way, reuse outbound connections with a keep-alive agent when calling other services {Dù sao, tái dùng kết nối outbound bằng keep-alive agent khi gọi service khác}:

import { Agent } from 'node:http';
// A shared agent pools sockets — avoids a TCP+TLS handshake on every call.
const agent = new Agent({ keepAlive: true, maxSockets: 50 });

3. A raw Node.js HTTP server {Server HTTP Node.js thô}

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ message: 'Hello' }));
    return;
  }
  res.statusCode = 404;
  res.end('Not Found');
});

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

Two facts that unlock everything later {Hai sự thật mở khóa mọi thứ về sau}: req is a Readable stream (the body arrives in chunks) and res is a Writable stream (you write status, headers, then body) {req là Readable stream (body đến theo chunk) và res là Writable stream (bạn ghi status, header, rồi body)}.

Headers before body, once {Header trước body, một lần}: once the body starts, headers lock. Writing them again throws ERR_HTTP_HEADERS_SENT — the most common beginner error {một khi body bắt đầu, header bị khóa. Ghi lại ném ERR_HTTP_HEADERS_SENT — lỗi người mới gặp nhiều nhất}. Use res.writeHead(status, headers) to set both at once {Dùng res.writeHead(status, headers) để đặt cả hai cùng lúc}.

A small response helper removes the repetition and the header-ordering bugs {Một helper response nhỏ loại bỏ lặp lại và bug thứ tự header}:

import type { ServerResponse } from 'node:http';

function sendJson(res: ServerResponse, status: number, body: unknown): void {
  const payload = JSON.stringify(body);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': Buffer.byteLength(payload), // bytes, not characters
  });
  res.end(payload);
}

4. Request parsing {Parse request}

URL & query parameters {URL & query parameter}

req.url is just a string (/search?q=node&page=2). Parse it with the URL class {req.url chỉ là chuỗi. Parse bằng class URL}:

const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
url.pathname;                    // '/search'
url.searchParams.get('q');       // 'node'
url.searchParams.getAll('tag');  // ['a','b'] for ?tag=a&tag=b
Number(url.searchParams.get('page') ?? '1'); // strings → coerce yourself

JSON body parsing — with limits {Parse body JSON — kèm giới hạn}

The body isn’t handed to you — collect the stream’s chunks, with a size cap to prevent abuse {Body không được trao sẵn — gom chunk của stream, kèm chốt kích thước chống lạm dụng}:

import type { IncomingMessage } from 'node:http';

async function readJson<T>(req: IncomingMessage, maxBytes = 1_000_000): Promise<T> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of req) {                 // req is async-iterable
    size += chunk.length;
    if (size > maxBytes) {
      const err = new Error('Payload too large');
      (err as Error & { status?: number }).status = 413; // signal 413 upstream
      throw err;
    }
    chunks.push(chunk as Buffer);
  }
  if (size === 0) return {} as T;
  return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T;
}

Look how much you must think about: streaming, a size limit, buffer concatenation, encoding, a parse that can throw {Nhìn xem phải nghĩ bao nhiêu: stream, giới hạn kích thước, nối buffer, encoding, parse có thể ném lỗi}. Express’s express.json() is exactly this in one line (Phase 3) {express.json() của Express chính là cái này trong một dòng (Phase 3)}.

Form-urlencoded & multipart (file uploads) {Form-urlencoded & multipart (upload file)}

HTML forms send application/x-www-form-urlencoded; parse with URLSearchParams {Form HTML gửi application/x-www-form-urlencoded; parse bằng URLSearchParams}. File uploads use multipart/form-data, where the body is split by a boundary into parts {Upload file dùng multipart/form-data, body bị chia bởi boundary thành nhiều phần}. Parsing multipart by hand is genuinely hard, so even seniors use a battle-tested parser like busboy {Parse multipart thủ công thật sự khó, nên cả senior cũng dùng parser dày dạn như busboy}:

import busboy from 'busboy';
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

function handleUpload(req: IncomingMessage): Promise<void> {
  return new Promise((resolve, reject) => {
    const bb = busboy({ headers: req.headers, limits: { fileSize: 10 * 1024 * 1024 } });
    bb.on('file', (_name, file, info) => {
      // Stream straight to disk — never buffer the whole upload in memory.
      void pipeline(file, createWriteStream(`uploads/${info.filename}`)).catch(reject);
    });
    bb.on('close', resolve);
    bb.on('error', reject);
    req.pipe(bb);
  });
}

The senior lesson {Bài học senior}: stream uploads to disk/object storage; never load a whole file into memory {stream upload ra đĩa/object storage; đừng bao giờ nạp cả file vào bộ nhớ} — the same backpressure principle as Phase 1 {cùng nguyên lý backpressure như Phase 1}.


5. A router by hand {Tự dựng router}

A handful of if (method && url) checks doesn’t scale, and doesn’t support path params {Vài lệnh if (method && url) không co giãn và không hỗ trợ path param}. Build a tiny matcher and you’ll understand exactly what Express does {Dựng một matcher nhỏ và bạn hiểu chính xác Express làm gì}:

import type { IncomingMessage, ServerResponse } from 'node:http';

type Params = Record<string, string>;
type Handler = (req: IncomingMessage, res: ServerResponse, params: Params) => void;
interface Route { method: string; pattern: RegExp; keys: string[]; handler: Handler }

const routes: Route[] = [];

function add(method: string, path: string, handler: Handler): void {
  const keys: string[] = [];
  // '/users/:id' → /^\/users\/([^/]+)$/  capturing each :param
  const pattern = new RegExp(
    '^' + path.replace(/:[^/]+/g, (m) => { keys.push(m.slice(1)); return '([^/]+)'; }) + '$',
  );
  routes.push({ method, pattern, keys, handler });
}

function route(req: IncomingMessage, res: ServerResponse): void {
  const { pathname } = new URL(req.url ?? '/', `http://${req.headers.host}`);
  const matches = routes.filter((r) => r.pattern.test(pathname));
  if (matches.length === 0) { res.writeHead(404).end('Not Found'); return; }

  const r = matches.find((m) => m.method === req.method);
  if (!r) { res.writeHead(405).end('Method Not Allowed'); return; } // path exists, verb doesn't

  const captured = r.pattern.exec(pathname)!.slice(1);
  const params = Object.fromEntries(r.keys.map((k, i) => [k, captured[i]]));
  r.handler(req, res, params);
}

add('GET', '/users/:id', (_req, res, params) => res.end(`user ${params.id}`));

Note the senior detail: when the path matches but the method doesn’t, the correct answer is 405 Method Not Allowed, not 404 {Lưu ý chi tiết senior: khi path khớp nhưng method không khớp, đáp án đúng là 405, không phải 404}.


6. Responses done right — streaming & SSE {Trả response đúng — streaming & SSE}

Because res is a Writable stream, you can stream big responses with flat memory and automatic backpressure {Vì res là Writable stream, bạn có thể stream response lớn với bộ nhớ phẳng và backpressure tự động}:

import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

// Stream a file back — never read it fully into memory first
res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
await pipeline(createReadStream('big.zip'), res);

Server-Sent Events (SSE) is a one-way stream of events over a single long-lived HTTP response — perfect for live notifications, progress, or token-by-token LLM output, with no WebSocket needed {Server-Sent Events (SSE) là luồng sự kiện một chiều trên một response HTTP sống lâu — hợp cho thông báo trực tiếp, tiến độ, hay output LLM theo từng token, không cần WebSocket}:

res.writeHead(200, {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  Connection: 'keep-alive',
});
const timer = setInterval(() => res.write(`data: ${Date.now()}\n\n`), 1000);
req.on('close', () => clearInterval(timer)); // stop when the client disconnects

Always clean up on req.on('close') for long-lived responses — otherwise you leak timers and memory per disconnected client {Luôn dọn dẹp ở req.on('close') cho response sống lâu — nếu không bạn rò rỉ timer và bộ nhớ cho mỗi client ngắt kết nối}.


A cookie is a small string the server sets and the browser returns on every later request — the standard way to carry a session id {Cookie là một chuỗi nhỏ server đặt và trình duyệt gửi lại ở mọi request sau — cách chuẩn để mang session id}:

res.setHeader('Set-Cookie', [
  // The four attributes that matter for security:
  `sid=${id}`,
  'HttpOnly',          // JS can't read it → blunts XSS token theft
  'Secure',            // HTTPS only
  'SameSite=Lax',      // not sent on cross-site POSTs → blunts CSRF
  'Path=/; Max-Age=86400',
].join('; '));

// Reading cookies back:
const cookies = Object.fromEntries(
  (req.headers.cookie ?? '').split('; ').filter(Boolean).map((c) => {
    const i = c.indexOf('=');
    return [c.slice(0, i), decodeURIComponent(c.slice(i + 1))];
  }),
);

Stateless vs stateful {Không trạng thái vs có trạng thái}: a session id + server-side store (Redis) is stateful (easy to revoke); a signed JWT is stateless (no lookup, but hard to revoke before expiry). We go deep on both in Phase 5 {một id session + store phía server (Redis) là có trạng thái (dễ thu hồi); một JWT ký là không trạng thái (không tra cứu, nhưng khó thu hồi trước hạn). Phase 5 sẽ đào sâu cả hai}.


8. CORS — from first principles {CORS — từ gốc}

The browser’s same-origin policy blocks JS from reading responses from a different origin unless the server opts in with CORS headers {Same-origin policy của trình duyệt chặn JS đọc response từ origin khác trừ khi server cho phép bằng header CORS}. For “non-simple” requests the browser first sends a preflight OPTIONS {Với request “không đơn giản” trình duyệt gửi trước một preflight OPTIONS}:

function applyCors(req: IncomingMessage, res: ServerResponse): boolean {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com'); // not '*' with credentials
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  if (req.method === 'OPTIONS') {           // preflight: answer and stop
    res.writeHead(204).end();
    return true;                            // handled
  }
  return false;
}

CORS is enforced by the browser, not the servercurl ignores it {CORS được thực thi bởi trình duyệt, không phải servercurl bỏ qua nó}. So CORS is not a security boundary; it’s a browser convenience/guardrail {Vậy CORS không phải ranh giới bảo mật; nó là tiện ích/lan can của trình duyệt}.


9. Caching & conditional requests {Cache & request có điều kiện}

The cheapest response is the one you don’t send {Response rẻ nhất là cái bạn không phải gửi}. Use Cache-Control to tell clients/proxies how long content is fresh, and ETag/Last-Modified for revalidation {Dùng Cache-Control để báo client/proxy nội dung còn tươi bao lâu, và ETag/Last-Modified để xác thực lại}:

import { createHash } from 'node:crypto';

const body = JSON.stringify(data);
const etag = `"${createHash('sha1').update(body).digest('base64')}"`;

if (req.headers['if-none-match'] === etag) {
  res.writeHead(304).end();                 // not modified — send nothing, save bandwidth
  return;
}
res.writeHead(200, { ETag: etag, 'Cache-Control': 'public, max-age=60' });
res.end(body);

10. Compression & content negotiation {Nén & thương lượng nội dung}

If the client advertises Accept-Encoding: gzip, br, compress the response stream {Nếu client báo Accept-Encoding: gzip, br, nén luồng response}:

import { createGzip, createBrotliCompress } from 'node:zlib';
import { pipeline } from 'node:stream/promises';

const accepts = req.headers['accept-encoding'] ?? '';
if (accepts.includes('br')) {
  res.writeHead(200, { 'Content-Encoding': 'br', Vary: 'Accept-Encoding' });
  await pipeline(source, createBrotliCompress(), res);
} else if (accepts.includes('gzip')) {
  res.writeHead(200, { 'Content-Encoding': 'gzip', Vary: 'Accept-Encoding' });
  await pipeline(source, createGzip(), res);
} else {
  await pipeline(source, res);
}

Set Vary: Accept-Encoding so caches store compressed and uncompressed variants separately {Đặt Vary: Accept-Encoding để cache lưu riêng bản nén và không nén}. In production you usually let Nginx do compression — but you must know how it works {Production thường để Nginx nén — nhưng bạn phải biết nó hoạt động thế nào}.


11. Security headers {Header bảo mật}

A handful of response headers harden any app — this is what helmet sets for you in Express {Một nhúm header response làm cứng app — đây là cái helmet đặt giúp bạn trong Express}:

res.setHeader('X-Content-Type-Options', 'nosniff');             // don't MIME-sniff
res.setHeader('X-Frame-Options', 'DENY');                       // no clickjacking via iframes
res.setHeader('Strict-Transport-Security', 'max-age=63072000'); // force HTTPS (HSTS)
res.setHeader('Content-Security-Policy', "default-src 'self'");  // restrict resource origins
res.setHeader('Referrer-Policy', 'no-referrer');

12. The middleware concept — discovered, not imported {Khái niệm middleware — khám phá, không phải import}

After a few routes, you notice the same needs everywhere: logging, body parsing, CORS, auth {Sau vài route, bạn nhận ra cùng nhu cầu khắp nơi: log, parse body, CORS, auth}. The insight: a request should flow through a pipeline of small functions, each able to inspect/modify it or pass control on {Insight: một request nên chảy qua pipeline các hàm nhỏ, mỗi hàm có thể xem/sửa nó hoặc chuyển quyền đi tiếp}.

request ─▶ [logger] ─▶ [cors] ─▶ [bodyParser] ─▶ [auth] ─▶ [handler] ─▶ response
              │           │           │            │           │
            next()      next()      next()       next()      res.end()

Build an async-aware version with centralized error handling {Dựng một phiên bản nhận biết async với xử lý lỗi tập trung}:

import type { IncomingMessage, ServerResponse } from 'node:http';

type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: () => Promise<void>,
) => void | Promise<void>;

function compose(middlewares: Middleware[]) {
  return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
    let i = -1;
    const dispatch = async (n: number): Promise<void> => {
      if (n <= i) throw new Error('next() called multiple times');
      i = n;
      const mw = middlewares[n];
      if (mw) await mw(req, res, () => dispatch(n + 1));
    };
    try {
      await dispatch(0);
    } catch (err) {
      if (!res.headersSent) res.writeHead(500).end('Internal Server Error');
      console.error(err);
    }
  };
}

That next() pattern is the heart of Express and Koa {Mẫu next() đó chính là trái tim của Express và Koa}. You just built a tiny version of it {Bạn vừa dựng một phiên bản nhỏ của nó}.


13. Timeouts & graceful shutdown {Timeout & tắt êm}

Production servers must defend against slow clients and must drain in-flight requests on deploy {Server production phải phòng client chậm và phải xả các request đang chạy khi deploy}.

const server = createServer(handler);

// Defend against slow-loris-style attacks and hung sockets:
server.requestTimeout = 30_000;     // max time to receive the full request
server.headersTimeout = 10_000;     // max time to receive headers
server.keepAliveTimeout = 5_000;    // idle keep-alive socket lifetime

server.listen(3000);

// Graceful shutdown: stop accepting, let active requests finish, then exit.
function shutdown(signal: string): void {
  console.log(`${signal} received — draining...`);
  server.close(() => { console.log('closed'); process.exit(0); });
  // Force-exit if something hangs past the grace period:
  setTimeout(() => process.exit(1), 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

server.close() stops new connections but waits for active ones — pair it with keepAliveTimeout so idle keep-alive sockets don’t hold shutdown open {server.close() dừng kết nối mới nhưng chờ kết nối đang chạy — kết hợp với keepAliveTimeout để socket keep-alive nhàn rỗi không giữ tiến trình tắt}. The .unref() lets the force-exit timer not keep the process alive on its own {.unref() để timer ép-thoát không tự giữ process sống}.


14. Hands-on projects {Dự án thực hành}

  1. REST API without Express {REST API không Express}: build a tasks API (GET /tasks, POST /tasks, GET/PUT/DELETE /tasks/:id) using only http, your hand-rolled router (with 405 handling), the URL class, and readJson. Return correct status codes and a consistent error shape {dựng API tasks chỉ bằng http, router tự dựng (có xử lý 405), class URL, và readJson. Trả đúng status code và hình dạng lỗi nhất quán}.

  2. Production middleware stack {Bộ middleware production}: with compose, layer logger (method url status duration-ms via res.on('finish')), CORS, JSON body parser (413 on oversize), and security headers {với compose, xếp lớp logger (qua res.on('finish')), CORS, parser body JSON (413 khi quá cỡ), và header bảo mật}.

  3. Streaming endpoints {Endpoint streaming}: add GET /download/:file (stream a file with pipeline), GET /events (SSE clock that cleans up on disconnect), and gzip/brotli negotiation {thêm GET /download/:file (stream file bằng pipeline), GET /events (đồng hồ SSE tự dọn khi ngắt), và thương lượng gzip/brotli}.

Extra drills {Bài tập thêm}: add ETag + 304 to the tasks list and prove a repeat request saves bandwidth; add requestTimeout and graceful shutdown, then confirm in-flight requests finish on SIGTERM; add an idempotency-key check to POST /tasks {thêm ETag + 304 cho danh sách tasks và chứng minh request lặp tiết kiệm băng thông; thêm requestTimeout và tắt êm, xác nhận request đang chạy hoàn tất khi SIGTERM; thêm kiểm tra idempotency-key cho POST /tasks}.


15. Senior checklist {Checklist senior}

  • I return the correct status code (incl. 405, 409, 422, 429) {Trả đúng status code}.
  • I cap body size and stream uploads to disk {Giới hạn body và stream upload ra đĩa}.
  • I set HttpOnly; Secure; SameSite cookies and the core security headers {Đặt cookie an toàn và header bảo mật}.
  • I handle CORS preflight correctly and know it’s browser-enforced {Xử lý preflight CORS đúng và biết nó do trình duyệt thực thi}.
  • I add caching/ETag where it helps and clean up long-lived responses {Thêm cache/ETag khi hữu ích và dọn response sống lâu}.
  • I set timeouts and shut down gracefully on SIGTERM {Đặt timeout và tắt êm khi SIGTERM}.

What’s next {Phần tiếp theo}

You understand HTTP at the protocol level (methods, status, headers, versions, keep-alive), you’ve built a real server with raw http — routing with params, streaming, SSE, cookies, CORS, caching, compression, security headers, timeouts and graceful shutdown — and you built the middleware concept yourself {Bạn hiểu HTTP ở mức giao thức, đã dựng server thật bằng http thô — routing có param, streaming, SSE, cookie, CORS, cache, nén, header bảo mật, timeout và tắt êm — và đã tự dựng khái niệm middleware}.

In Phase 3, we bring in Express.js and watch every pain point dissolve — declarative routing, the middleware pipeline, built-in body parsing, validation, and a robust error-handling strategy including the async error wrapper every Express app needs {Ở Phase 3, ta đưa Express.js vào và xem mọi điểm đau tan biến — routing khai báo, pipeline middleware, parse body dựng sẵn, validation, và chiến lược xử lý lỗi vững chắc gồm async error wrapper mà mọi app Express cần}.