
Web Development Essentials 035.3: Khái niệm cơ bản về SQL
Tạo bảng SQLite, hiểu khóa chính và CHECK, dùng CRUD có WHERE và bind tham số từ Node.js; kiểm tra transaction rollback, mở lại dữ liệu và API driver.

Tạo route Express, phục vụ CSS và render EJS; đọc query, params, body, kiểm tra dữ liệu form, escape HTML và phân biệt XSS với CSRF qua lab loopback.
Một form gửi thành công nhưng server không đọc được nội dung. Khi thêm parser, lập trình viên lại echo chuỗi vào HTML mà chưa escape. Để xây một ứng dụng nhỏ dễ kiểm tra, cần biết middleware nào chạy trước, dữ liệu đến từ đâu và phản hồi được tạo trong ngữ cảnh nào. Express giúp tổ chức các bước ấy, nhưng không tự biến mọi đầu vào thành dữ liệu đáng tin.
Bài 035.2, Khái niệm cơ bản về NodeJS Express, tham chiếu giáo trình 030-100 trang in 304–338. Nội dung được viết độc lập, đối chiếu ngày 23/10/2026. Bạn cần biết Node/npm, module, HTTP, form và DOM. Mục tiêu là tạo route, phục vụ CSS tĩnh, render một mẫu EJS không lồng, đọc query/body/params và kiểm tra đầu vào trên server.
Một route gắn phương thức HTTP với đường dẫn và hàm xử lý. App.get xử lý GET; app.post xử lý POST. Đường dẫn URL không phải đường dẫn tệp trên ổ đĩa. Request tới /guide có thể được tạo từ template mà không tồn tại tệp guide ở thư mục public. Express cung cấp đối tượng request và response, thường đặt tên req và res.
Middleware được đăng ký theo thứ tự. Nó có thể thêm dữ liệu vào request, gửi phản hồi hoặc chuyển quyền xử lý bằng next. Nếu middleware gửi phản hồi rồi vẫn chạy tiếp không có chủ đích, ứng dụng có thể cố gửi header lần hai. Nếu quên kết thúc hoặc chuyển tiếp, request có thể treo. Vẽ đường đi của một request thường hữu ích hơn thêm app.use ở vị trí ngẫu nhiên.
Parser phải đứng trước route cần dữ liệu nó tạo. Express.urlencoded xử lý body application/x-www-form-urlencoded; Express.json xử lý JSON. Không cần thêm package body-parser riêng chỉ để dùng hai parser tích hợp này. Multipart là định dạng khác và không được urlencoded tự xử lý; lab không upload tệp.
Req.params chứa giá trị của tham số route như /guide/:code. Req.query chứa dữ liệu query string. Req.body chứa body sau parser phù hợp, có thể undefined nếu không có bước phân tích tương ứng. Đây là thuộc tính request, không phải res.query hoặc res.body như lỗi trong danh sách thuật ngữ PDF.
Giá trị từ client phải được kiểm tra kiểu và miền. Query lặp tên có thể cho array thay vì một chuỗi; body cũng có thể thiếu thuộc tính. Đừng gọi trim trực tiếp trên một giá trị chỉ vì bạn mong nó là string. Accept mô tả định dạng phản hồi mà client chấp nhận; Content-Type mô tả định dạng dữ liệu đang gửi. Hai header không thay thế nhau.
Req.ip phụ thuộc kết nối và cấu hình proxy; nó không luôn là IP công khai của người dùng cuối. Không bật trust proxy tùy tiện rồi tin mọi header X-Forwarded-For. User-Agent cũng do client kiểm soát. Nếu cần đọc header có dấu gạch ngang, dùng req.get("user-agent") hoặc cú pháp ngoặc vuông, không viết phép trừ ngoài ý định bằng req.headers.user-agent.
Express.static phục vụ tệp từ thư mục được chỉ định. Chỉ đặt tài sản công khai như CSS ở đó; không đưa .env, mã cấu hình bí mật hoặc bản sao dữ liệu vào public. Dùng đường dẫn tuyệt đối xác định từ module để không phụ thuộc thư mục terminal khởi động. Mount /assets không yêu cầu người dùng gửi tên thư mục public trong URL.
EJS là template engine thực thi template tin cậy phía server. <% ... %> chạy điều khiển không trực tiếp xuất giá trị; <%= ... %> xuất có escape; <%# ... %> là comment EJS; <%- ... %> xuất raw. Escape mặc định phù hợp ngữ cảnh HTML liên quan, không phải công cụ bảo vệ mọi ngữ cảnh URL, JavaScript hay CSS. Trong lab, dữ liệu chỉ xuất vào text của HTML qua <%=.
Không cho client chọn nội dung template, filename render hoặc options tùy ý. Chỉ truyền các trường dữ liệu đã lựa chọn; không đưa nguyên req.query vào res.render. Template do người dùng điều khiển là vấn đề thực thi mã, khác với một chuỗi thông báo được escape đúng. Phân biệt template tin cậy và dữ liệu không tin cậy ngay khi thiết kế.

XSS là việc nội dung không tin cậy dẫn tới thực thi mã trong ngữ cảnh trang. CSRF lợi dụng việc browser gửi credential theo yêu cầu để thực hiện hành động không mong muốn trên ứng dụng mà người dùng đang có phiên; nó không nhất thiết đánh cắp cookie hoặc cần chèn JavaScript vào trang đích. Cả hai có thể tương tác nhưng không cùng cơ chế.
Cookie định danh DEMO không chứng minh danh tính. Khi triển khai phiên thật, cần thiết kế session, cookie Secure/HttpOnly/SameSite phù hợp, kiểm tra quyền phía server và cơ chế chống CSRF cho hành động dùng credential tự động. Chỉ cài session middleware không hoàn tất các yêu cầu đó. HTTPS bảo vệ đường truyền nhưng không sửa XSS hoặc validation sai.
Lab dưới không có tài khoản, cookie phiên hay ghi dữ liệu. POST chỉ trả bản xem trước để học parsing và template, nên không quảng bá nó thành ứng dụng nhận yêu cầu production. Không nhập dữ liệu cá nhân. Đây là phạm vi đủ nhỏ để kiểm tra request/response mà chưa phải xây một hệ thống danh tính.
Trong thư mục mới, dùng Node 24 LTS và npm. Các phiên bản package được chọn theo registry chính thức ngày 23/10/2026: Express 5.3.0, EJS 7.0.2. Đây là pin cho lab, không phải lời hứa luôn dùng nguyên phiên bản này khi xuất bản đợt sau. Kiểm tra lại advisory và hỗ trợ trước khi áp dụng vào dự án thật.
npm init --yes
npm pkg set type=module
npm pkg set private=true --json
npm install --save-exact --ignore-scripts --no-audit --no-fund express@5.3.0 ejs@7.0.2
mkdir public views
Giữ lockfile cùng mã nguồn. Cờ ignore-scripts chỉ giới hạn lifecycle script khi cài, không ngăn mã thư viện chạy lúc import. Đọc phụ thuộc trước khi sử dụng; không cần sudo hoặc cài global. File public/style.css có thể dùng:
body { font-family: system-ui, sans-serif; color: #213641; line-height: 1.6; }
main { max-width: 40rem; margin: auto; padding: 1rem; }
input, button { font: inherit; padding: 0.5rem; }
:focus-visible { outline: 3px solid #b45309; outline-offset: 3px; }
Lưu views/guide.ejs. Mẫu giữ nhãn và trường form trong HTML; server chỉ đưa title và message vào ngữ cảnh text. Comment EJS không được gửi xuống như comment HTML.
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= title %></title>
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<main>
<h1><%= title %></h1>
<%# Chỉ dùng dữ liệu DEMO trong bản xem trước. %>
<form action="/preview" method="post">
<label for="subject">Triệu chứng DEMO, tối đa 80 ký tự</label>
<input id="subject" name="subject" maxlength="80" required>
<button type="submit">Xem trước</button>
</form>
<% if (message !== "") { %>
<p id="message"><%= message %></p>
<% } %>
</main>
</body>
</html>
Lưu app.js. Route query dùng để chọn kênh hướng dẫn, route params nhận mã DEMO cố định, còn POST nhận chuỗi triệu chứng. Ba nguồn dữ liệu được tách để người học không nhầm tên thuộc tính. Parser có giới hạn kích thước và số tham số riêng, validation có giới hạn độ dài nghiệp vụ riêng.
import express from "express";
import { fileURLToPath } from "node:url";
const app = express();
app.disable("x-powered-by");
app.set("view engine", "ejs");
app.set("views", fileURLToPath(new URL("./views/", import.meta.url)));
app.use("/assets", express.static(fileURLToPath(new URL("./public/", import.meta.url))));
app.use(express.urlencoded({ extended: false, limit: "1kb", parameterLimit: 5 }));
const title = "Hướng dẫn DEMO";
app.get("/", (req, res) => {
const channel = req.query.channel;
if (channel !== undefined && channel !== "mail" && channel !== "chat") {
return res.status(400).type("text").send("Kênh không hợp lệ");
}
res.render("guide", { title, message: channel ? "Kênh: " + channel : "" });
});
app.get("/guide/:code", (req, res) => {
if (req.params.code !== "DEMO") {
return res.status(404).type("text").send("Không có hướng dẫn");
}
res.json({ code: req.params.code, mode: "preview-only" });
});
app.post("/preview", (req, res) => {
if (!req.is("application/x-www-form-urlencoded")) {
return res.status(415).type("text").send("Định dạng chưa hỗ trợ");
}
const subject = req.body?.subject;
if (typeof subject !== "string" || subject.trim() === "" || subject.length > 80) {
return res.status(400).type("text").send("Triệu chứng không hợp lệ");
}
res.render("guide", { title, message: subject.trim() });
});
app.use((req, res) => res.status(404).type("text").send("Không tìm thấy route"));
app.use((error, req, res, next) => {
if (res.headersSent) return next(error);
const status = error.status === 413 ? 413 : error.status === 400 ? 400 : 500;
res.status(status).type("text").send(status === 500 ? "Lỗi xử lý DEMO" : "Body không hợp lệ");
});
app.listen(3000, "127.0.0.1", () => console.log("Express DEMO loopback:3000"));
Chạy node app.js, mở /?channel=mail. Kỳ vọng HTML có tiêu đề và “Kênh: mail”, CSS từ /assets/style.css tải được. Thử /guide/DEMO nhận JSON, /guide/OTHER nhận 404. Query channel lặp hai lần hoặc giá trị chưa hỗ trợ phải nhận 400 thay vì được chuyển thành chuỗi tùy tiện.
Gửi form với nội dung “Máy in DEMO”. Kết quả hiện trong p message; không có bản ghi nào được lưu. Thử chuỗi vô hại <b>DEMO</b>: trang phải hiện nguyên dấu ngoặc, không tạo phần tử b trong vùng kết quả. Đây là kiểm tra escape trong ngữ cảnh text của mẫu này, không phải chứng minh mọi XSS đã được loại bỏ ở mọi nơi.
Dùng HTTP client gửi body rỗng, subject lặp tên, chuỗi dài hơn 80 ký tự hoặc JSON vào /preview. Kỳ vọng lần lượt 400, 400, 400 và 415. Body urlencoded vượt giới hạn 1kb phải bị parser từ chối với 413. Required/maxlength của browser chỉ hỗ trợ trải nghiệm; các trường hợp gửi trực tiếp này xác nhận server không dựa riêng vào kiểm tra client.

Lab đã chạy ngày 23/10/2026 với Node 24.22.0, Express 5.3.0 và EJS 7.0.2 trong container không có mạng ngoài, user không phải root. HTTP client xác nhận route, CSS, query lặp, body rỗng/lặp/dài, sai MIME, giới hạn 413 và escape; dừng server làm cổng ngừng phục vụ. Chromium 151.0.7922.34 render phản hồi EJS thật, xác nhận văn bản có ngoặc không thành node b, label chuyển focus và Tab tới nút có outline. Chưa gửi form bằng browser, thử session, CSRF token, database, TLS hoặc production.
Nếu req.body undefined, kiểm tra Content-Type, vị trí parser và việc middleware đã chạy, không chỉ thêm package mới. Nếu CSS 404, kiểm tra mount URL cùng thư mục public. Nếu template không có giá trị, phân biệt thẻ điều khiển <% với xuất giá trị <%=. Nếu server gửi stack ra client, kiểm tra error handler và cấu hình môi trường thay vì xem stack là nội dung hợp lệ.
Dừng server bằng Ctrl+C, xác nhận cổng lab không còn phục vụ. Với mục tiêu Essentials, bạn hoàn thành khi giải thích được thứ tự middleware, ba nguồn đầu vào, các phản hồi lỗi và vị trí escape. Deployment, session thật, CSRF token và lưu dữ liệu cần thiết kế riêng trước khi ứng dụng tiếp nhận thông tin thật.

Tạo bảng SQLite, hiểu khóa chính và CHECK, dùng CRUD có WHERE và bind tham số từ Node.js; kiểm tra transaction rollback, mở lại dữ liệu và API driver.

Chạy Node.js LTS, phân biệt ES module và CommonJS, package.json và lockfile; cài package DEMO cục bộ, dùng npm ci và kiểm tra server HTTP trên loopback.

Chọn node DOM, phân biệt collection sống và tĩnh, textContent và innerHTML; cập nhật class, thuộc tính và sự kiện nút bằng chuột, bàn phím trong lab DEMO.

Tổ chức hàm JavaScript với if, switch, vòng lặp và return; hiểu truthy, so sánh, break, continue và xử lý Promise bằng async/await qua dữ liệu DEMO.