GĐ04 — Express: cơ chế & cấu trúc project (Dự án 1: Todo API)
Ghi chú cho FE engineer (JS/TS mạnh) chuyển sang Backend. Mỗi khái niệm: định nghĩa → tại sao quan trọng → cơ chế → code ngắn → pitfall. Mindset chuyển đổi: ở FE bạn gọi API; ở BE bạn là cái API đó — bạn nhận request thô, tự parse, tự validate, tự quyết định response và status code.
Hộp phiên bản — Express 5. Mọi ví dụ trong file chạy trên Express 5.2 (
npm i express@5, cần Node ≥ 18). Nhiều bài viết cũ vẫn viết cho Express 4; khác biệt cần nhớ:
- Async handler tự chuyển lỗi. Handler
asyncmàthrow(hoặc promise reject) → Express 5 tự gọinext(err). Không cầnexpress-async-errors,asyncHandlerhaytry/catchchỉ để chuyển lỗi. Trên Express 4 cùng đoạn code làm request treo và sinhunhandledRejection(đã chạy thử cả hai bản).- Wildcard phải đặt tên:
app.get("/files/*splat", …);"/files/*"ném lỗi ngay lúc đăng ký route.req.params.splatlà mảng các đoạn path.req.bodylàundefinedkhi không có parser nào khớpContent-Type(hoặc client không gửi body). Luôn xử lý trường hợp này trước khi đọc field.req.querylà getter chỉ đọc: gánreq.query = …némTypeError. Validate xong thì lưu kết quả vào biến mới.- Dự án còn ở Express 4 (4.22.x)? Giữ
asyncHandlerở mục 6 (hoặc dùngexpress-async-errors).- Kiểm chứng ngày 2026-10-05 (npm registry, cài và chạy trong thư mục tạm):
express5.2.1,helmet8.3.0,jsonwebtoken9.0.3,bcryptjs3.0.3,supertest7.3.1,zod4.6.5, Node 24.21,tsc --strict(TypeScript 7.0.2) không lỗi. Mã trong lời giải Dự án 1 đã chạy thật với các bản này. Nguồn phiên bản: https://registry.npmjs.org/ và https://nodejs.org/en/about/previous-releases.
1. Express là gì — quan hệ với http module#
Định nghĩa. Express là một framework tối giản (thin layer) bọc quanh module
http built-in của Node. Bản chất Express không thay thế http; nó chỉ thêm
lớp routing (map method + path → handler) và middleware (chuỗi hàm xử lý
request tuần tự). Cuối cùng Express vẫn gọi http.createServer().
Tại sao quan trọng. Nếu chỉ dùng http thô, bạn phải tự viết if (req.url === '/todos' && req.method === 'GET'), tự parse body từ stream, tự set header. Không
scale được. Express biến mớ if/else đó thành khai báo route + middleware sạch sẽ.
Hiểu rằng Express chỉ là lớp bọc giúp bạn không "sợ" nó — khi debug, bạn biết
req/res vẫn là http.IncomingMessage/http.ServerResponse được mở rộng thêm.
Cơ chế. app = express() trả về một request listener function (req, res).
Function này có thể truyền thẳng vào http.createServer(app). Bên trong, mỗi
request đi qua một stack middleware; Express duyệt stack, tìm cái nào match path
- method rồi gọi tuần tự.
Pitfall. Đừng nhầm Express là "server". Nó là handler. Khi bạn cần thêm
WebSocket (socket.io) hay HTTPS, bạn cần đối tượng http.Server để dùng chung cổng.
app.listen() có trả về chính http.Server đó (const server = app.listen(3000)),
nên dùng cách nào cũng lấy được tham chiếu; http.createServer(app) chỉ rõ ràng hơn
khi bạn muốn tự chọn https.createServer(opts, app) hoặc gắn thêm listener trước khi
listen.
2. Routing: method, params, query, Router modular#
Định nghĩa. Routing là việc map (HTTP method, URL path) tới một handler.
Express cung cấp app.get/post/put/patch/delete(path, handler). Path có thể chứa
route params (/todos/:id) và request kèm query string (/todos?page=2).
Tại sao quan trọng. Đây là "bảng định tuyến" của API — hợp đồng giữa client và server. Route params dùng cho định danh tài nguyên (resource id); query dùng cho tùy chọn (filter, sort, pagination). Phân biệt đúng giúp API RESTful, dễ đoán.
Cơ chế.
req.params— object từ các:nametrong path (luôn là string).req.query— object parse từ query string (giá trị là string hoặc string[]).express.Router()— mini-app con để nhóm route theo domain, mount bằngapp.use("/prefix", router). Giúp tách file, tránh 1 file router khổng lồ.
Kết quả (đã chạy trên Express 5.2.1, Node 24): khai báo /todos/stats trước /todos/:id thì GET /todos/stats ra {"route":"stats"}; GET /todos/42 ra id: "42" kiểu string; GET /todos?page=5 với q.query.page + 1 ra "51", không phải 6.
Pitfall.
req.params.idvàreq.query.pageluôn là string, không phải number. Ép kiểu + validate trước khi dùng, nếu khôngid + 1="5" + 1="51".- Thứ tự route quan trọng: route cụ thể (
/todos/stats) phải đặt trước route động (/todos/:id), nếu không:idnuốt luônstats.
3. Middleware chain & next()#
Định nghĩa. Middleware là hàm (req, res, next) chạy tuần tự trong pipeline
xử lý request. Mỗi middleware có 3 lựa chọn: (a) gọi next() để chuyển cho cái kế
tiếp, (b) kết thúc bằng res.send/json/end (early return), hoặc (c) gọi
next(err) để nhảy sang error handler.
Tại sao quan trọng. Đây là xương sống của Express. Auth, logging, body
parsing, validation, rate limit — tất cả đều là middleware. Hiểu chuỗi này = hiểu
80% Express. Nó cũng là nguồn bug phổ biến nhất (quên next() → request treo).
Cơ chế. Express giữ một stack. Với mỗi request, nó gọi middleware đầu tiên
match; next() là con trỏ "đi tiếp". Nếu không gọi next() và cũng không trả
response → request treo mãi mãi (client timeout). Middleware có thể:
- Global:
app.use(fn)— chạy cho mọi request. - Route-level:
app.get("/x", mw1, mw2, handler)— chỉ cho route đó.
Luồng một request đi qua stack (thứ tự đăng ký = thứ tự chạy):
Kết quả (đã chạy): middleware không gọi next() và không trả response thì fetch tới route đó hết thời gian chờ (TimeoutError), server không báo lỗi gì.
Pitfall.
- Quên
next(): request treo. Triệu chứng: Postman quay mãi. - Gọi
next()rồi vẫnres.json(): lỗiCannot set headers after they are sent. Sau khi trả response phảireturnngay. - Thứ tự đăng ký = thứ tự chạy.
app.use(express.json())phải nằm trước route đọcreq.body. Đăng ký sau route thì route không thấy body.
4. Body parsing: express.json(), limit, raw body cho webhook#
Định nghĩa. Body của POST/PUT đến dưới dạng stream byte thô. express.json()
là middleware đọc hết stream, JSON.parse rồi gán vào req.body. Không có nó,
req.body là undefined.
Tại sao quan trọng. FE quen await res.json() ở client. Ở server, bạn mới là
người phải làm bước đó. Đây là bug "kinh điển" của người mới: POST data lên nhưng
req.body rỗng vì quên đăng ký parser.
Cơ chế. express.json() chỉ parse khi header Content-Type: application/json.
Tham số limit giới hạn kích thước body (mặc định 100kb) để chống DoS bằng
payload khổng lồ. express.urlencoded() cho form HTML.
Raw body cho webhook. Webhook (Stripe, GitHub) ký payload bằng HMAC trên chuỗi
byte gốc. Nếu để express.json() parse thành object rồi JSON.stringify lại,
byte có thể khác (thứ tự key, khoảng trắng) → verify chữ ký thất bại. Phải giữ
raw body cho đúng route đó:
Kết quả (đã chạy): POST /echo không có body, hoặc có Content-Type: text/plain, cho req.body === undefined; gửi application/json {"a":1} thì req.body là {a:1}. Route /webhook đăng ký express.raw trước express.json() nhận Buffer đúng 12 byte của chuỗi { "a" : 1 } (kể cả khoảng trắng), nên HMAC tính trên byte gốc.
Pitfall. Đặt express.json() global rồi mới thêm route webhook → raw body
đã bị nuốt. Fix: đăng ký express.raw() cho route webhook trước global json,
hoặc dùng express.json({ verify }) để lưu buffer gốc.
5. Validation với Zod#
Định nghĩa. Zod là thư viện schema validation ưu tiên TypeScript. Bạn khai báo schema mô tả hình dạng dữ liệu; Zod kiểm tra runtime và suy ra type tự động.
Tại sao quan trọng. req.body/req.query là dữ liệu không tin được từ
client. TypeScript chỉ kiểm tra lúc compile, không bảo vệ runtime — client có thể
gửi bất cứ thứ gì. Validate ở biên (boundary) chặn dữ liệu bẩn trước khi nó chạm
tới business logic. "Parse, don't validate": sau khi qua Zod, bạn có object đã
đúng type, không phải rải if (typeof x...) khắp nơi.
Cơ chế. schema.parse(data) — throw ZodError nếu sai. schema.safeParse(data)
— trả { success, data | error }, không throw. Trong middleware thường dùng
safeParse để tự kiểm soát response.
Pitfall.
- Dùng
422 Unprocessable Entitycho lỗi validate (semantics rõ ràng hơn400). - Query string toàn string → dùng
z.coerce.number()để ép"2"→2. - Đừng dùng lại
req.bodygốc sau validate; dùngresult.data(đã có default, đã strip field lạ nếu schema strict).
6. Error handling tập trung#
Định nghĩa. Express nhận diện error-handling middleware bằng 4 tham số
(err, req, res, next). Khi bất kỳ đâu gọi next(err), Express nhảy thẳng tới
handler 4-tham-số này, bỏ qua mọi middleware thường ở giữa.
Tại sao quan trọng. Không có nó, mỗi route phải tự try/catch rồi res.status
lặp đi lặp lại → trùng lặp, dễ sót, response lỗi không đồng nhất. Một global
handler = một chỗ duy nhất map lỗi → HTTP response, log tập trung.
Cơ chế. Ba mảnh ghép:
AppErrorclass — lỗi có chủ đích (operational) mang theostatusCode. Phân biệt với bug lập trình (programmer error) để biết cái nào an toàn để lộ ra client, cái nào phải giấu (500).
- Lỗi từ
asynchandler — trên Express 5 mộtasyncroutethrow(hoặc promise reject) được chuyển thẳng sangnext(err), không cần wrapper. Trên Express 4 Express không thấy lỗi đó → request treo, nên phải bọc để chuyển reject thànhnext(err)(chỉ cần nếu bạn còn dùng Express 4):
- Global handler (đăng ký cuối cùng, sau mọi route). Nó phải map cả lỗi
không phải
AppErrormà client có thể gây ra:ZodError(input sai) và lỗi củaexpress.json()(JSON hỏng, body quá lớn). Thiếu hai nhánh này thì chúng rơi hết xuống 500, trái với lời hứa "input sai → 422/400":
Kiểm tra bằng curl (đã chạy trên Express 5.2.1 + Zod 4):
Pitfall.
- Trên Express 4, quên
asyncHandlerlà bug #1 với async route: throw biến mất, request treo. Express 5 hết bẫy này. ZodErrorvà lỗi body-parser không phảiAppError. Handler chỉ nhậnAppErrorthìschema.parse()sai input và JSON hỏng đều thành 500 (đã đo: 500 cho cả hai).- Error handler phải đủ 4 tham số kể cả không dùng
next— nếu chỉ 3 tham số, Express coi nó là middleware thường, không nhận lỗi. - Đăng ký
errorHandlertrước route → nó không bao giờ chạy. Luôn cuối cùng.
7. Cấu trúc project theo layer#
Định nghĩa. Tách code thành các tầng trách nhiệm rõ ràng:
route → controller → service → repository.
| Layer | Trách nhiệm | KHÔNG làm |
|---|---|---|
| route | khai báo path + gắn middleware | không có logic |
| controller | đọc req, gọi service, trả res + status | không truy vấn DB, không business rule |
| service | business logic, orchestration, transaction | không biết req/res |
| repository | truy cập DB (query thô) | không có business rule |
Tại sao quan trọng. Là FE bạn quen tách component/hook/api-client — cùng tư
duy separation of concerns. Nhồi tất cả vào route handler thì: không test unit
được (service dính chặt req/res), không tái sử dụng logic, một file phình 800
dòng. Tách tầng giúp test service độc lập, đổi DB chỉ sửa repository.
Cơ chế. Dữ liệu chảy xuống, kết quả chảy lên. Controller là dịch giả giữa thế giới HTTP và business logic; service thuần TypeScript, không import gì từ express → dễ unit test.
Pitfall. Đừng "over-engineer" ngay từ đầu (KISS/YAGNI): với Todo API nhỏ,
controller → service là đủ; repository chỉ cần khi query DB phức tạp/nhiều nơi.
Nhưng tuyệt đối không để service import express hay đụng res — mất khả năng
test và tái dùng.
8. Auth JWT middleware#
Định nghĩa. JWT (JSON Web Token) là token tự chứa (self-contained), ký bằng
secret. Access token chứng minh danh tính người dùng trong mỗi request. Middleware
auth verify token, giải mã payload, gắn req.user để các handler sau dùng.
Tại sao quan trọng. HTTP stateless — mỗi request độc lập, server không nhớ ai
là ai. JWT giải bài toán đó: client gửi kèm token ở header
Authorization: Bearer <token>; server verify chữ ký (không cần query DB session)
→ biết user id.
Cơ chế. Flow cơ bản: login → server ký JWT (jwt.sign(payload, secret)) →
client lưu và gửi lại ở mỗi request → middleware jwt.verify(token, secret):
- Chữ ký sai / token hết hạn → throw → trả
401. - Hợp lệ → gán
req.user = payload,next().
Pitfall.
- Không pin
algorithmstrongjwt.verifylà lỗi hay gặp: server chấp nhận token do chính bên gửi chọn thuật toán. Với secret dạng chuỗi,jsonwebtoken9 vẫn nhận HS256/HS384/HS512; token ký HS512 bằng đúng secret đi qua nếu không pin (đã chạy: thiếualgorithmsthì test "token HS512" ra 200 thay vì 401; riêngalg: nonethìjsonwebtoken9 đã từ chối khi có secret). Khi chuyển sang khoá bất đối xứng (RS256/ES256), pin sai kiểu là đường vào của tấn công nhầm kiểu khoá. - Đừng nhét dữ liệu nhạy cảm (password) vào JWT payload — nó chỉ encode base64, ai cũng đọc được, chỉ không sửa được nhờ chữ ký.
- Access token nên hết hạn ngắn (
expiresIn: "15m"); dùng refresh token cho phiên dài (nâng cao, GĐ sau). - Phân biệt 401 (chưa xác thực / token sai) vs 403 (đã xác thực nhưng không
đủ quyền, vd thiếu role) — hai tầng khác nhau:
authgác 401, kiểm quyền gác 403. Với tài nguyên của người khác (todo của user khác) trả 404, không phải 403: 403 xác nhận "id này có thật" và cho kẻ dò id biết nên thử tiếp. Quy ước xuyên suốt lộ trình, lập luận ở GĐ12 mục 7.
9. Phân trang (pagination)#
Định nghĩa. Kỹ thuật trả một phần danh sách thay vì toàn bộ. Kiểu phổ biến
nhất: offset/limit — limit (số item mỗi trang) + offset/page (bỏ qua bao
nhiêu).
Tại sao quan trọng. SELECT * FROM todos với 1 triệu dòng sẽ giết cả DB và
response. Pagination bảo vệ server và cho client tải dần. Không thể có API list
production nào mà thiếu nó.
Cơ chế. offset = (page - 1) * limit. Query LIMIT limit OFFSET offset. Kèm
một query COUNT(*) để trả metadata (total, totalPages) giúp client render
UI phân trang.
Pitfall.
- Thiếu
orderByhoặc sort không duy nhất (hai todo cùngcreatedAt): DB được phép trả thứ tự khác nhau giữa hai lần gọi, nên một item có thể xuất hiện ở hai trang hoặc biến mất. Luôn sort theo cột ổn định kèmidlàm tie-breaker (khung lời giải Dự án 1 làm vậy). - Luôn clamp
limit(.max(100)) — client gửilimit=999999= DoS. - Offset lớn (page 5000) chậm dần vì DB vẫn phải quét qua. Với dataset khổng lồ
dùng cursor pagination (
WHERE id > lastId) — nâng cao. - Nhớ đính kèm điều kiện
userIdkhi count, nếu khôngtotalsai (đếm cả của người khác).
10. Config & secrets#
Định nghĩa. Config là các giá trị thay đổi theo môi trường (port, DB URL, JWT
secret). Secrets là config nhạy cảm. dotenv nạp file .env vào
process.env.
Tại sao quan trọng. Hardcode secret trong code = rò rỉ khi push GitHub =
thảm họa bảo mật. Tách config theo môi trường (dev/staging/prod) là chuẩn
12-factor. Việc validate env lúc boot giúp app fail fast: thiếu JWT_SECRET
thì crash ngay khi khởi động, không phải lúc request đầu tiên vào production.
Cơ chế. Nạp .env → validate bằng Zod → export object env đã typed. App chỉ
import từ module này, không đọc process.env rải rác.
Pitfall.
- Commit
.envlà lỗi chết người — luôn.gitignorenó, cung cấp.env.example(không giá trị thật) để đồng đội biết cần key gì. - Đọc
process.env.Xtrực tiếp trong code → mất type + mất validate. Luôn quaenv. - Trên Railway/Render, không upload
.env; nhập biến qua dashboard của platform.
11. Logging request cơ bản#
Định nghĩa. Ghi log mỗi HTTP request (method, path, status, thời gian xử lý).
morgan (đơn giản) hoặc pino-http (JSON có cấu trúc, nhanh) là middleware phổ
biến.
Tại sao quan trọng. Production không có DevTools Network tab. Khi user báo
"API lỗi lúc 3h chiều", log là bằng chứng duy nhất để điều tra: request nào,
status gì, mất bao lâu. console.log rải rác không đủ — cần format nhất quán, có
timestamp, có request id.
Cơ chế. Đăng ký như global middleware, đặt sớm trong stack để bắt mọi request. Ở prod dùng structured JSON (dễ đẩy vào Datadog/Grafana); ở dev dùng format người-đọc-được.
Pitfall.
- Đừng log body chứa secret (password, token) — log cũng là nơi rò rỉ dữ liệu. Redact các field nhạy cảm.
- Đặt logger sau body parser nếu muốn log body, nhưng trước route để đo đúng
thời gian. Với error, để error handler log riêng (mức
error).
12. Header bảo mật với helmet#
Định nghĩa. helmet là middleware đặt một bộ header HTTP phòng thủ cho mọi response (X-Content-Type-Options: nosniff, Strict-Transport-Security, Content-Security-Policy, Referrer-Policy...) và bỏ header X-Powered-By để không khoe "Express".
Tại sao quan trọng. Đây là lớp rẻ nhất trong "bảo mật cấu hình" (OWASP A02): một dòng code, chặn được cả một họ lỗi (trình duyệt đoán sai kiểu nội dung, nhúng trang vào iframe lạ, hạ cấp xuống HTTP). Nó không thay thế auth, CORS hay rate limit, chỉ bổ sung. Chi tiết các lớp còn lại ở GĐ09.
Cơ chế. app.use(helmet()) gọi từng middleware con với mặc định an toàn. Đăng ký đầu tiên, trước log và parser, để cả response lỗi và 404 cũng có header.
Kết quả (đã chạy, helmet 8.3.0, curl -i localhost:3000/health/live trên server thật): response có Content-Security-Policy, Strict-Transport-Security: max-age=31536000; includeSubDomains, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: no-referrer, và không có X-Powered-By.
Pitfall.
Strict-Transport-Securitychỉ có tác dụng khi trình duyệt truy cập qua HTTPS. Sau reverse proxy hoặc CDN đã tự thêm HSTS thì đừng cấu hình hai nơi lệch nhau.helmetkhông bật CORS và không chặn request nào:curlvẫn gọi được như thường. CORS cấu hình riêng (GĐ03 mục 4).- Tắt
contentSecurityPolicycho cả app chỉ vì "bị chặn script" là sửa sai chỗ: chỉ nới đúng nguồn cần dùng. X-XSS-Protection: 0là cố ý (bộ lọc XSS cũ của trình duyệt từng gây lỗ hổng); đừng "sửa" thành1.
Dự án 1 — Todo/Notes API#
Mục tiêu: ráp toàn bộ 11 khái niệm trên thành một REST API chạy được và deploy lên internet.
Yêu cầu chức năng.
- Auth JWT:
POST /auth/register,POST /auth/login→ trả access token. Middlewareauthbảo vệ mọi route/todos. - CRUD todos (scoped theo user — chỉ thấy/sửa todo của mình):
POST /todos— tạo (validate title).GET /todos— list + pagination (?page=&limit=) + meta.GET /todos/:id— chi tiết (404 nếu không có hoặc của người khác — không lộ id nào có thật).idlà UUID dạng chuỗi (crypto.randomUUID()nếu lưu in-memory;@default(uuid())ở GĐ05), không phải số tự tăng.PATCH /todos/:id— cập nhật (title/done).DELETE /todos/:id— xóa.
- Validation: mọi body/query qua Zod middleware → 422 khi sai.
- Error handling tập trung:
AppError+ global handler (Express 5 tự chuyển lỗi async; xử lý cảZodErrorvà JSON hỏng). - Deploy: Railway hoặc Render (Postgres managed + env qua dashboard).
Cấu trúc gợi ý.
Thứ tự ráp middleware trong app.ts (quan trọng):
helmet(header bảo mật) → 2.morgan(log) → 3.express.json()→ 4. mount routers →- 404 handler → 6.
errorHandler(CUỐI cùng).
Lời giải và cách kiểm tra: Dự án 1 — Todo/Notes API
Đã chạy: khung dưới đây (lưu in-memory) chạy với Node 24.21, Express 5.2.1, Zod 4.6, jsonwebtoken 9, helmet 8.3, bcryptjs 3, tsc --strict không lỗi; mọi kết quả ở "Kết quả" (kể cả dòng morgan không log password) là quan sát thật. Sáu test supertest ở cuối lời giải chạy xanh (node --import tsx --test). Phần deploy chưa chạy (cần tài khoản Railway/Render).
Hướng làm. (1) env.ts validate biến môi trường, thoát ngay nếu sai. (2) Dựng app.ts đúng thứ tự: log, express.json, router, 404, errorHandler. (3) Làm module auth (register hash bằng bcryptjs, login ký JWT 15 phút), rồi module todos có userId ở mọi truy vấn. (4) Chạy kịch bản curl ở dưới đối chiếu từng dòng Done khi. (5) Khi sang GĐ05, chỉ thay phần lưu trữ trong *.service.ts; route và middleware giữ nguyên.
Sơ đồ.
Code tham chiếu (rút gọn; thư mục như "Cấu trúc gợi ý" ở trên, import dùng đuôi .js theo NodeNext của GĐ02):
Test tự động (node:test có sẵn trong Node, cộng supertest; GĐ13 mới giới thiệu Vitest nên ở đây chưa cần). createApp() không listen, supertest tự mở cổng tạm:
Chạy: JWT_SECRET=<32+ ký tự> NODE_ENV=test node --import tsx --test test/todo-api.test.ts. Bản đầy đủ đã chạy gồm thêm ca đăng ký trùng email (409), sai mật khẩu và email lạ ra cùng một thân 401, PATCH/DELETE/phân trang, 404 cho route lạ và kiểm header helmet: 6 test, 0 lỗi. Gỡ algorithms: ["HS256"] khỏi jwt.verify thì test HS512 fail (token đi qua với 200 thay vì 401), nên test này bảo vệ đúng điều nó nói.
Ghi chú: id là crypto.randomUUID(); schema đăng nhập dùng z.email() (Zod 4, z.string().email() đã deprecated); mật khẩu giới hạn max(72) vì bcrypt chỉ dùng 72 byte đầu.
Kết quả (.env có PORT=4420 và JWT_SECRET dài 32+ ký tự; chạy npx tsx src/server.ts, rồi curl localhost:4420):
| Lệnh | Kết quả quan sát |
|---|---|
GET /todos không token | 401 |
POST /todos với {"title":""} | 422 {"error":"ValidationError","details":{"title":[...]}} |
POST /todos không body, không Content-Type | 422 (req.body là undefined, ?? {} cứu) |
GET /todos?page=abc | 422, details.page |
GET /todos?limit=999 | 422, details.limit: Too big (quy định giới hạn bằng từ chối, không lẳng lặng cắt xuống 100) |
POST /todos với -d '{bad' | 400 Malformed JSON body |
POST /todos body JSON 2 MB | 413 request entity too large |
User B GET /todos/<id của A> | 404 {"error":"Not found"}, byte-for-byte giống GET /todos/<uuid lạ> |
| Token sai chữ ký, và token đã hết hạn | đều 401 Invalid or expired token |
POST /todos hợp lệ | 201, id dạng UUID; GET /todos?page=1&limit=10 có meta { page, limit, total, totalPages } |
Lỗi bất ngờ (throw new TypeError("...password=xyz") trong handler async) | 500 {"error":"Internal Server Error"}; chi tiết chỉ ở log server |
Thiếu hoặc ngắn JWT_SECRET | process in Invalid env: {...} rồi exit=1 |
morgan("dev") chỉ log method, path, status, thời gian: grep password trong log ra 0 dòng.
Lỗi hay gặp. Đặt errorHandler trước router (không bao giờ chạy). Quên đuôi .js trong import khi dùng NodeNext. Dùng req.params.id như string trong TypeScript qua chuỗi middleware có thể báo string | string[] với @types/express 5: bọc String(req.params.id). Trả 403 cho todo của người khác (lộ id có thật). Đăng ký trùng email mà không trả 409, hoặc login trả thông báo khác nhau cho "email lạ" và "sai mật khẩu" (cho kẻ dò biết email nào có tài khoản). jwt.verify không pin algorithms. list quên lọc userId khi đếm: total đếm cả của người khác. Deploy: nhập JWT_SECRET qua dashboard của nền tảng, không đẩy .env; kiểm bằng curl https://<app>/health/live.
Done khi#
-
express.json()đăng ký trước route; POST đọc đượcreq.body.Đáp án
Thứ tự đăng ký là thứ tự chạy; parser đăng ký sau route thì route đó thấy
req.body === undefined. Tự kiểm:curl -X POST -H 'content-type: application/json' -d '{"a":1}'tới route echo ra{a:1}; không có headerContent-Typethìundefined. Sai thường gặp: quên rằng Express 5 không còn gán{}mặc định. Xem GĐ04 mục 4. -
Mọi input (body + query) validate bằng Zod; sai → 422 kèm chi tiết field.
Đáp án
Dùng
safeParsetrong middlewarevalidate(body) vàschema.parse(req.query)(némZodError, handler cuối map sang 422). Kết quả mong đợi:{"error":"ValidationError","details":{"title":[...]}}. Dùngz.coerce.number()cho query vì mọi giá trị query là string. Xem GĐ04 mục 5. -
Async route
throwở service → global handler bắt (Express 5 không cần wrapper; Express 4 thì bọcasyncHandler).Đáp án
Express 5 tự gọi
next(err)khi promise reject, nên không cần wrapper. Tự kiểm: routeasync () => { throw new AppError(409, "x") }trả409, route némTypeErrortrả500(không treo). Sai thường gặp: vẫn bọcasyncHandlertrên Express 5 (vô hại nhưng thừa), hoặc quên bọc trên Express 4 (request treo). Xem GĐ04 mục 6. -
GET /todos?page=abc→ 422 (không phải 500); POST body JSON hỏng → 400; body vượtlimit→ 413. Kiểm bằngcurl -i.Đáp án
curl -i "localhost:4420/todos?page=abc"ra422;-d '{bad'ra400 Malformed JSON body(nhánherr.type === "entity.parse.failed"); body trênlimitra413(nhánherr.status4xx). Thiếu hai nhánh cuối thì cả hai rơi xuống500. Xem GĐ04 mục 6. -
AppErrorphân biệt operational (lộ message) vs 500 (giấu chi tiết, log).Đáp án
isOperational = true(404, 409...) thì lộmessage; mọi thứ khác trả đúngInternal Server Errorvà chi tiết chỉ vào log. Tự kiểm: némnew TypeError("password=xyz"); response không chứaxyz, log server có. Xem GĐ04 mục 6. -
/todoschỉ truy cập được khi có JWT hợp lệ; sai/hết hạn → 401; truy cập todo người khác → 404 (giống hệt todo không tồn tại).Đáp án
Không token, token sai chữ ký, token hết hạn đều
401. Todo của user A đọc bằng token B ra404 {"error":"Not found"}, giống từng byte với id không tồn tại: truy vấn luôn lọcuserIdrồi mới quyết định. Sai thường gặp: trả403(xác nhận id có thật). Xem GĐ04 mục 8 và GĐ12 mục 7. -
GET /todosphân trang, trảdata+meta { page, limit, total, totalPages };limitbị clamp.max(100).Đáp án
offset = (page - 1) * limit, hai truy vấn (danh sách và đếm) cùng điều kiệnuserId, trảmeta { page, limit, total, totalPages }.limit=999bị schema.max(100)từ chối với422(clamp bằng từ chối), không phải âm thầm cắt xuống. Sai thường gặp: đếm không lọcuserIdnêntotalsai. Xem GĐ04 mục 9. -
Không hardcode secret;
.envtrong.gitignore; env validate lúc boot, thiếu key → crash ngay.Đáp án
grep -rn JWT_SECRET srcchỉ thấyconfig/env.tsvà nơi dùngenv.JWT_SECRET, không có chuỗi secret.git check-ignore .envin ra.env. XoáJWT_SECRETkhỏi môi trường rồi chạy server: phải thấyInvalid env: {...}và exit code1ngay lúc khởi động. Xem GĐ04 mục 10. -
Layer tách bạch: service không import
express, không đụngreq/res.Đáp án
grep -rn "from \"express\"" src/modules/*/*.service.tsra rỗng (đã chạy: "no express in service"); service nhận tham số thuần (userId,page), némAppError, không đụngreq/res. Nhờ vậy test service không cần dựng HTTP. Xem GĐ04 mục 7. -
Request logging bật; không log field nhạy cảm.
Đáp án
morganchỉ ghi method, path, status, thời gian, không ghi body. Tự kiểm: gửiPOST /auth/registervới password, rồigrep passwordtrong log phải ra 0 dòng. Nếu chuyển sangpino-http, đặtredactchoreq.headers.authorization. Xem GĐ04 mục 11. -
Deploy Railway/Render chạy được; test bằng URL public (Postman/curl).
Đáp án
Chưa chạy (cần tài khoản nền tảng). Cách tự kiểm: biến môi trường nhập qua dashboard, rồi
curl -i https://<app>/health/livera200,curl -i https://<app>/todosra401. Nền tảng thường truyềnPORTqua biến môi trường nênenv.PORTphải được đọc, không hardcode. -
Đăng ký trùng email → 409; login sai mật khẩu và login email lạ trả cùng một thân 401;
passwordHashkhông nằm trong response nào.Đáp án
Đã chạy trong bộ test của lời giải:
registerlần hai cùng email ra409; hai lần login lỗi (sai mật khẩu, email không tồn tại) chodeepEqualthân{"error":"Invalid credentials"};registerchỉ trả{ id, email }. Lý do: thông báo khác nhau cho phép kẻ tấn công liệt kê email đã đăng ký. Mật khẩu băm bằngbcryptjs(giới hạnmax(72)vì bcrypt chỉ dùng 72 byte đầu). Xem GĐ04 mục 8 và hashing ở GĐ09. -
Có ít nhất 3 test
supertestxanh (todo của người khác → 404, token sai thuật toán → 401, JSON hỏng/query sai → 400/422);jwt.verifypinalgorithms;helmetđăng ký đầu tiên.Đáp án
JWT_SECRET=<32+ ký tự> NODE_ENV=test node --import tsx --test test/*.test.tsphải báofail 0. Kiểm test có giá trị: tạm gỡ{ algorithms: ["HS256"] }khỏijwt.verify, test token HS512 phải đỏ (đã chạy: đỏ), trả lại thì xanh.curl -i localhost:3000/health/livethấyX-Content-Type-Options: nosniffvà không cóX-Powered-By. Xem GĐ04 mục 12 và lời giải Dự án 1.
Câu hỏi mở#
-
Chọn ORM nào (Prisma vs Drizzle vs knex thô) cho repository layer? — ảnh hưởng cách viết repo ở GĐ sau.
Hướng trả lời hiện tại
(Chưa khẳng định.) lộ trình dùng Prisma ở GĐ05, nên Dự án 1 giữ repository mỏng và chỉ đổi phần lưu trữ. Drizzle hay Kysely hợp lý nếu bạn muốn SQL gần hơn; chưa có số đo trong tài liệu này để so sánh.
-
Refresh token / logout / token revoke: để GĐ nâng cao hay đưa vào ngay Dự án 1?
Hướng trả lời hiện tại
(Chưa khẳng định.) để sau. Dự án 1 chỉ cần access token hết hạn ngắn; refresh token kéo theo bảng lưu phiên và xoay vòng token, gấp đôi phạm vi. Xem phần auth ở GĐ07.
-
DB thật (Postgres) hay in-memory array để tập trung học Express trước? — nếu mục tiêu là "cơ chế Express", có thể bắt đầu bằng array rồi thay bằng Postgres sau.
Hướng trả lời hiện tại
(Chưa khẳng định.) bắt đầu bằng
Mapin-memory (khung trong lời giải làm đúng như vậy), vì mọi Done khi ở GĐ04 kiểm được mà không cần DB; chuyển sang Postgres ở GĐ05 mà route không đổi.