const requestTimer: RequestHandler = (req, res, next) => {
const start = process.hrtime.bigint();
res.on("finish", () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
logger.info({ method: req.method, path: req.path, status: res.statusCode, ms });
});
next();
};
Express.js
draftedMiddleware chain, async error handling (Express 4 vs 5), security middleware, trust proxy, cấu trúc app lớn, graceful shutdown, Express vs Fastify vs NestJS.
0/45 rated · mastery 0%
Bài học
10 bài · ~124 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~13 phút· 7 câuverify
Middleware pipeline, routing và Router
Express thực chất là một stack các layer chạy theo thứ tự đăng ký: app.use vs app.get, next() và các biến thể, Router với mergeParams, req.params/query/body, và vì sao response gửi hai lần sinh ERR_HTTP_HEADERS_SENT.
- 02~12 phút· 6 câuverify
Error handling: error middleware, async trong Express 4 và 5
Express nhận diện error middleware bằng số tham số, lỗi đi theo đường nào tới nó, vì sao async handler trên Express 4 làm treo request rồi crash process, Express 5 sửa được gì và chưa sửa gì, và cách thiết kế error handling tập trung.
- 03~12 phút· 5 câuverify
Express 5: thay đổi, path-to-regexp v8, ReDoS và kế hoạch migrate
Những gì Express 5 thật sự đổi so với Express 4: cú pháp route mới và vì sao (ReDoS CVE-2024-45296), API bị xoá, thay đổi âm thầm như req.query getter hay res.send(number), và cách migrate một codebase hàng trăm route an toàn.
- 04~11 phút· 4 câuverify
Validation, typed request, mass assignment và request context
Validate body/query/params bằng zod trong một middleware tái sử dụng, truyền dữ liệu đã xác thực tới handler type-safe (res.locals vs req.user), chặn mass assignment và prototype pollution, và giữ requestId/tenantId qua AsyncLocalStorage mà không mất context.
- 05~14 phút· 3 câuverify
Security middleware, CORS, trust proxy và rate limiting
Bộ middleware bảo mật cho một Express API public: helmet làm gì, giới hạn body và timeout, CORS preflight và credentials, trust proxy quyết định req.ip ra sao, và vì sao cấu hình sai làm rate limiter chặn tất cả hoặc không chặn ai.
- 06~13 phút· 3 câuverify
Authentication, RBAC và pipeline multi-tenant
Thiết kế pipeline request cho một Express API multi-tenant: verify JWT đúng cách (JWKS, alg, iss/aud/exp), Google sign-in qua OIDC, lấy tenant từ nguồn tin cậy, RBAC coarse-grained ở middleware và resource-level ở query, rate limit theo tenant, và các lỗ hổng thường gặp.
- 07~13 phút· 5 câuverify
Static files, download, upload và HTTP caching
Serve static an toàn (dotfiles, cache header), download chống path traversal, vì sao upload vào memory làm pod OOMKilled và cách thay bằng stream/pre-signed URL, ETag/304 và Cache-Control, khi nào không dùng compression middleware, và Redis cache ở middleware hay service.
- 08~13 phút· 4 câuverify
Timeouts, client disconnect, graceful shutdown và event loop
Các lớp timeout của một Express service (server, route, outbound), huỷ công việc khi client bỏ đi, 502 lẻ tẻ sau ALB vì keepAliveTimeout, health check và graceful shutdown trên Kubernetes, và một endpoint CPU nặng làm tăng p99 của cả API.
- 09~11 phút· 4 câuverify
Cấu trúc codebase lớn, testing, idempotency và refactor an toàn
Tổ chức Express theo module với controller mỏng, service không biết HTTP và composition root; tách app khỏi server để test bằng supertest; một endpoint checkout chống double submit bằng idempotency key; và cách refactor legacy mà không đổi hành vi.
- 10~13 phút· 4 câuverify
Express vs Fastify vs NestJS, và chuyển dần từ monolith
So sánh Express, Fastify và NestJS bằng số đo thật thay vì cảm tính, khi nào đổi framework là quyết định tốt hay tệ, và cách chuyển endpoint từ monolith sang service Express mới bằng strangler fig, shadow traffic và canary mà client cũ không vỡ.
Tóm tắt & cheat sheet của track
TL;DR
- Express là một router + chuỗi middleware mỏng trên
httpcủa Node. Mọi thứ (body parsing, auth, CORS, logging, error handling) đều là middleware chạy theo thứ tự đăng ký. - Express 5 (5.0 ra 09/2024, thành
latesttrên npm từ 5.1 — verify) thay đổi quan trọng: promise bị reject trong handler/middleware được tự chuyển tới error handler; cú pháp path mới (path-to-regexp v8: wildcard phải có tên/*splat, optional dùng{});req.querychỉ là getter;req.bodylàundefinedkhi body parser không parse request (v4 là{}); yêu cầu Node 18+. - Senior interview hỏi: thứ tự middleware, xử lý lỗi async,
trust proxy, bảo mật (helmet, CORS, rate limit, body size), cấu trúc codebase lớn, graceful shutdown, và khi nào chọn Fastify/NestJS.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Middleware | (req, res, next) => void, xử lý rồi gọi next() hoặc kết thúc response | app.use(express.json()) |
| Error middleware | 4 tham số (err, req, res, next), chỉ chạy khi có lỗi | app.use((err, req, res, next) => res.status(500)...) |
| Router | Mini-app có middleware/route riêng, mount theo prefix | app.use("/orders", ordersRouter) |
next(err) | Bỏ qua middleware thường, nhảy tới error middleware | next(new NotFoundError()) |
next("route") / next("router") | Bỏ qua các handler còn lại của route / của router | Feature flag route |
res.locals | Dữ liệu sống trong một request | res.locals.user = user |
trust proxy | Tin header X-Forwarded-* từ proxy để tính req.ip, req.protocol | app.set("trust proxy", 1) |
app.param | Middleware chạy khi route có param đó | Load :orderId một lần |
Cơ chế hoạt động
- Middleware không match path thì bị bỏ qua;
app.use("/api", mw)match theo prefix,app.get("/api")match chính xác. - Handler phải hoặc gửi response, hoặc gọi
next()— không làm cả hai, không làm cái nào (request treo). - Error middleware đặt cuối cùng, sau tất cả route.
Express 4 vs 5: async error
Trade-offs
| Express | Fastify | NestJS | |
|---|---|---|---|
| Triết lý | Tối giản, không ý kiến | Hiệu năng, schema-first, plugin encapsulation | Framework đầy đủ, DI, module, decorator |
| Hiệu năng overhead | Trung bình | Thấp (JSON schema serialization) | Phụ thuộc adapter (Express/Fastify) + tầng DI |
| Validation | Tự chọn (zod, joi) | JSON Schema built-in | Pipes + class-validator / schema |
| Async error | Express 5 tự xử lý; 4 cần wrapper | Built-in | Built-in (exception filters) |
| Cấu trúc codebase lớn | Tự xây convention | Plugin giúp tách | Có sẵn convention (module/provider) |
| Ecosystem middleware | Lớn nhất | Tốt, có @fastify/express compat | Dùng lại middleware Express |
| Hợp khi | Service nhỏ-vừa, team tự kỷ luật | API throughput cao | Team lớn, domain phức tạp |
Pitfalls
- Quên
returnsaures.json()trong nhánh lỗi → "Cannot set headers after they are sent". - Express 4 + async handler không có wrapper → rejection không tới error middleware: request treo, và Node 15+ crash vì
unhandledRejection. - Error middleware khai báo 3 tham số → Express coi là middleware thường.
- Đặt
express.json()sau router, hoặc CORS sau route → không có tác dụng. trust proxysai:req.iplà IP load balancer → rate limit chặn tất cả user cùng lúc, hoặc cho phép client giảX-Forwarded-For.- Không giới hạn body size / upload lưu memory → DoS/OOM.
- Trả
err.stackhoặc lỗi DB thô cho client. - Upgrade lên v5 mà không sửa route pattern (
*,?, regex) → route không match hoặc lỗi lúc khởi động.
Cheat sheet
- Thứ tự đề xuất: security headers → CORS → body parser (có
limit) → requestId/logging → auth → routes → 404 → error handler. next()tiếp ·next(err)tới error handler ·next("route")bỏ phần còn lại của route ·next("router")thoát router.res.headersSentđể kiểm tra trước khi ghi response trong error handler (nếu đã gửi thìnext(err)để Express đóng connection).- Express 5 path (verify):
/*splat(wildcard có tên, không match root),/{*splat}(có cả root),/:file{.:ext}(optional), không còn regex inline trong string path. - Express 5 xoá:
app.del,req.param(),res.send(status),res.redirect("back"), chữ kýres.json(obj, status);express.urlencodedmặc địnhextended: false;express.staticmặc định bỏ qua dotfiles. express.json()mặc địnhlimit: "100kb".
References
- Express — Using middleware ↗
- Express — Error handling ↗
- Express — Migrating to Express 5 ↗
- Express — Routing ↗
- Express — Behind proxies (trust proxy) ↗
- Express — Security best practices ↗
- Express — Performance best practices ↗
- Express — Health checks and graceful shutdown ↗
- Express 5.x API reference ↗
- Node.js API — HTTP server timeouts ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 45 / 45
const orders = express.Router({ mergeParams: true });
orders.get("/:orderId", (req, res) => {
res.json({ tenantId: req.params.tenantId, orderId: req.params.orderId });
});
app.use("/tenants/:tenantId/orders", orders);
app.get("/orders/:id", async (req, res) => {
const order = await repo.find(req.params.id);
if (!order) res.status(404).json({ error: "not found" }); // missing return
res.json(order); // throws ERR_HTTP_HEADERS_SENT when not found
});
// Express 4
const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>) =>
(req: Request, res: Response, next: NextFunction) => { fn(req, res, next).catch(next); };
app.get("/users/:id", asyncHandler(async (req, res) => {
res.json(await users.get(req.params.id));
}));
// Express 5: the plain async handler is enough
app.get("/checkout", (req, res, next) => (flags.newCheckout(req) ? next("route") : next()), legacyCheckout);
app.get("/checkout", newCheckout);
const app = express();
app.use("/api/orders", ordersRouter);
app.use(express.json());
app.use(cors({ origin: "https://shop.example.com", credentials: true }));
app.use(authenticate);
app.use("/api/admin", adminRouter);
app.use(errorHandler);
const validate = <S extends z.ZodTypeAny>(schema: S, where: "body" | "query" | "params") =>
(req: Request, res: Response, next: NextFunction) => {
const r = schema.safeParse(req[where]);
if (!r.success) return next(new ValidationError(r.error.issues));
res.locals[where] = r.data; // don't reassign req.query in Express 5
next();
};
router.post("/orders", validate(CreateOrder, "body"), createOrder);
app.set("trust proxy", 1); // exactly one proxy (e.g. ALB) in front
app.use(rateLimit({ windowMs: 60_000, limit: 100, keyGenerator: (req) => req.user?.id ?? req.ip! }));
app.get("*", spaFallback); // catch-all
app.get("/files/*", serveFile); // nested path
app.get("/export/:id.:format?", exportOrder); // optional format
app.get("/:lang(en|vi)/products", products); // inline regex
app.get("/invoices/:id/pdf", async (req, res) => {
const inv = await invoices.findForTenant(req.tenantId, req.params.id);
if (!inv) return res.sendStatus(404);
res.sendFile(inv.storageKey, { root: "/data/invoices", dotfiles: "deny" });
});
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) return next(err);
const e = toAppError(err); // maps known libs (zod, DB driver) to AppError
if (e.status >= 500) logger.error({ err, requestId: res.locals.requestId }, "unhandled");
res.status(e.status).json({ type: e.code, title: e.expose ? e.message : "Internal error", requestId: res.locals.requestId });
});
// express@4
app.get("/orders/:id", async (req, res) => {
const order = await orders.get(req.params.id); // throws when DB times out
res.json(order);
});
app.use((err, req, res, next) => res.status(500).json({ error: "internal" }));
const requirePermission = (perm: string): RequestHandler => (req, res, next) => {
permissions.forUser(req.user.id).then((perms) => {
if (!perms.includes(perm)) {
res.status(403).json({ error: "forbidden" });
}
next();
});
next();
};
app.use((err: AppError, req: Request, res: Response) => {
res.status(err.status ?? 500).json({ code: err.code });
});
app.use("/api", apiRouter);
app.use((req, _res, next) => {
req.query = sanitize(req.query); // worked on Express 4
req.body = sanitize(req.body);
next();
});
app.patch("/me", async (req, res) => {
await db.user.update({ where: { id: req.user.id }, data: req.body }); // client sends {"role":"admin"}
res.sendStatus(204);
});
let ready = true;
app.get("/readyz", (_req, res) => res.sendStatus(ready ? 200 : 503));
process.once("SIGTERM", async () => {
ready = false;
await sleep(5_000);
server.close(async () => { await db.end(); process.exit(0); });
server.closeIdleConnections();
setTimeout(() => process.exit(1), 20_000).unref();
});