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}
| Method | Purpose {Mục đích} | Safe {An toàn} | Idempotent |
|---|---|---|---|
| GET | read {đọc} | yes | yes |
| HEAD | read headers only {chỉ header} | yes | yes |
| POST | create {tạo} | no | no |
| PUT | replace fully {thay toàn bộ} | no | yes |
| PATCH | partial update {sửa một phần} | no | no |
| DELETE | remove {xóa} | no | yes |
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
POSTsafe to retry, accept an idempotency key header and de-duplicate server-side {đểPOSTan 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 {401 là chưa xác thực; 403 là đã 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 quaAccept}.
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.1 — keep-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/2 — multiplexing: 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émERR_HTTP_HEADERS_SENT— lỗi người mới gặp nhiều nhất}. Useres.writeHead(status, headers)to set both at once {Dùngres.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}.
7. Cookies & sessions {Cookie & session}
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 server —
curlignores it {CORS được thực thi bởi trình duyệt, không phải server —curlbỏ 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-Encodingso caches store compressed and uncompressed variants separately {ĐặtVary: 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 withkeepAliveTimeoutso 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ớikeepAliveTimeoutđể 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}
-
REST API without Express {REST API không Express}: build a
tasksAPI (GET /tasks,POST /tasks,GET/PUT/DELETE /tasks/:id) using onlyhttp, your hand-rolled router (with405handling), theURLclass, andreadJson. Return correct status codes and a consistent error shape {dựng APItaskschỉ bằnghttp, router tự dựng (có xử lý405), classURL, vàreadJson. Trả đúng status code và hình dạng lỗi nhất quán}. -
Production middleware stack {Bộ middleware production}: with
compose, layer logger (method url status duration-msviares.on('finish')), CORS, JSON body parser (413 on oversize), and security headers {vớicompose, xếp lớp logger (quares.on('finish')), CORS, parser body JSON (413 khi quá cỡ), và header bảo mật}. -
Streaming endpoints {Endpoint streaming}: add
GET /download/:file(stream a file withpipeline),GET /events(SSE clock that cleans up on disconnect), and gzip/brotli negotiation {thêmGET /download/:file(stream file bằngpipeline),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; SameSitecookies 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 khiSIGTERM}.
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}.