Skip to content
DevRecall
← Roadmap
06

Express.js

drafted

Middleware 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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 http củ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 latest trê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.query chỉ là getter; req.body là undefined khi 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ệmMột câuVí dụ ngắn
Middleware(req, res, next) => void, xử lý rồi gọi next() hoặc kết thúc responseapp.use(express.json())
Error middleware4 tham số (err, req, res, next), chỉ chạy khi có lỗiapp.use((err, req, res, next) => res.status(500)...)
RouterMini-app có middleware/route riêng, mount theo prefixapp.use("/orders", ordersRouter)
next(err)Bỏ qua middleware thường, nhảy tới error middlewarenext(new NotFoundError())
next("route") / next("router")Bỏ qua các handler còn lại của route / của routerFeature flag route
res.localsDữ liệu sống trong một requestres.locals.user = user
trust proxyTin header X-Forwarded-* từ proxy để tính req.ip, req.protocolapp.set("trust proxy", 1)
app.paramMiddleware 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

ExpressFastifyNestJS
Triết lýTối giản, không ý kiếnHiệu năng, schema-first, plugin encapsulationFramework đầy đủ, DI, module, decorator
Hiệu năng overheadTrung bìnhThấp (JSON schema serialization)Phụ thuộc adapter (Express/Fastify) + tầng DI
ValidationTự chọn (zod, joi)JSON Schema built-inPipes + class-validator / schema
Async errorExpress 5 tự xử lý; 4 cần wrapperBuilt-inBuilt-in (exception filters)
Cấu trúc codebase lớnTự xây conventionPlugin giúp táchCó sẵn convention (module/provider)
Ecosystem middlewareLớn nhấtTốt, có @fastify/express compatDùng lại middleware Express
Hợp khiService nhỏ-vừa, team tự kỷ luậtAPI throughput caoTeam lớn, domain phức tạp

Pitfalls

  • Quên return sau res.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 proxy sai: req.ip là 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.stack hoặ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.urlencoded mặc định extended: false; express.static mặc định bỏ qua dotfiles.
  • express.json() mặc định limit: "100kb".

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 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();
};
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();
});