Vấn đề: Bạn đang dùng sequelize.sync({ alter: true }) để tự động thay đổi database schema mỗi khi server khởi động.
Tại sao lại sai? Hãy tưởng tượng database là một cái tủ hồ sơ. alter: true giống như mỗi lần mở cửa hàng, bạn tự động sắp xếp lại toàn bộ tủ — có thể vô tình làm mất dữ liệu. Trong production (server thật có người dùng thật), điều này cực kỳ nguy hiểm vì có thể xóa cột, mất data.
Cách fix: Dùng database migrations — giống như viết nhật ký thay đổi database. Mỗi lần bạn muốn thay đổi cấu trúc bảng, bạn viết 1 file migration mô tả thay đổi đó. Như vậy bạn kiểm soát được từng bước thay đổi và có thể rollback (quay lại) nếu cần.
Từ khóa tìm hiểu: Sequelize migrations, database migration là gì, sequelize-cli migration
Vấn đề: API login/register không giới hạn số lần gọi.
Tại sao lại sai? Hacker có thể viết script gửi hàng triệu request login với mật khẩu khác nhau để đoán password (gọi là brute force attack). Vì server không giới hạn, nó sẽ xử lý tất cả request đó — vừa dễ bị hack, vừa có thể làm sập server.
Cách fix: Dùng thư viện express-rate-limit. Ví dụ: giới hạn mỗi IP chỉ được gọi /auth/login tối đa 5 lần trong 15 phút. Giống như bảo vệ ở cửa nói "anh thử sai 5 lần rồi, đợi 15 phút nha".
Từ khóa tìm hiểu: express-rate-limit, brute force attack là gì, API rate limiting
Vấn đề: Khi xóa 1 Campaign, các bản ghi CampaignRecipient liên quan vẫn còn trong database.
Tại sao lại sai? Giống như bạn xóa 1 lớp học nhưng danh sách điểm danh của lớp đó vẫn nằm trong hệ thống, không ai quản lý. Dữ liệu "mồ côi" này tích tụ dần sẽ làm database phình to và gây lỗi logic.
Cách fix: Thêm onDelete: 'CASCADE' vào association. Nghĩa là: "khi xóa Campaign, tự động xóa luôn tất cả CampaignRecipient liên quan". Giống như xóa lớp thì xóa luôn sổ điểm danh.
Từ khóa tìm hiểu: Sequelize onDelete CASCADE, foreign key cascade, orphaned records là gì
Vấn đề: Frontend hiển thị nội dung HTML email bằng dangerouslySetInnerHTML mà không lọc (sanitize).
Tại sao lại sai? Nếu ai đó nhập nội dung email chứa <script>alert('hacked')</script>, trình duyệt sẽ chạy đoạn script đó. Đây gọi là XSS (Cross-Site Scripting) — hacker có thể đánh cắp cookie, token đăng nhập, hoặc điều khiển tài khoản người dùng.
React đặt tên dangerouslySetInnerHTML dài và xấu có chủ đích — để nhắc bạn rằng nó nguy hiểm.
Cách fix: Dùng thư viện dompurify để lọc bỏ tất cả thẻ nguy hiểm trước khi hiển thị. Hoặc render HTML trong <iframe sandbox> để cô lập nó.
Từ khóa tìm hiểu: XSS attack là gì, dompurify React, dangerouslySetInnerHTML security, OWASP XSS
Vấn đề: API trả về toàn bộ danh sách campaign và recipient, không giới hạn.
Tại sao lại sai? Khi có 10 campaign thì không sao. Nhưng khi có 100,000 campaign, server phải query hết, serialize hết, gửi hết qua mạng — cực chậm, tốn RAM, có thể crash. Giống như bạn tìm 1 cuốn sách nhưng thư viện ship toàn bộ 100,000 cuốn về nhà bạn thay vì cho bạn xem từng trang catalog.
Cách fix: Thêm query params ?page=1&limit=20 vào API. Trong Sequelize dùng offset và limit. Trả thêm metadata: tổng số bản ghi, tổng số trang, trang hiện tại.
Từ khóa tìm hiểu: API pagination, Sequelize offset limit, cursor-based pagination vs offset
Vấn đề: Việc gửi email chạy trong bộ nhớ (RAM) bằng setImmediate. Nếu server restart giữa chừng, tất cả email đang gửi bị mất.
Tại sao lại sai? Hãy tưởng tượng bạn viết danh sách việc cần làm lên bảng trắng (RAM). Ai đó tắt đèn (server restart) → bảng trắng bị xóa sạch → bạn không biết mình đã làm gì, chưa làm gì. Trong production, nếu campaign có 10,000 người nhận và server restart ở email thứ 5,000, bạn không biết ai đã nhận, ai chưa.
Cách fix lý tưởng: Dùng message queue như BullMQ hoặc RabbitMQ. Giống như viết danh sách vào sổ (database/Redis) thay vì bảng trắng — tắt đèn rồi bật lại vẫn còn. Nếu chưa kịp implement, ít nhất nên ghi rõ trade-off này trong README.
Từ khóa tìm hiểu: BullMQ tutorial, message queue là gì, background job Node.js, job queue vs setImmediate
Vấn đề: Chỉ có 3 unit test cơ bản cho utility functions. Không có test cho API endpoints, services, hay luồng nghiệp vụ.
Tại sao lại quan trọng? Test giống như kiểm tra chất lượng trước khi giao hàng. Bạn có 1 hệ thống gửi email campaign nhưng không test luồng gửi email? Interviewer sẽ hỏi: "Sao bạn biết code chạy đúng?"
Nên thêm ít nhất:
- 1 integration test: Test luồng tạo campaign → gửi → kiểm tra status
- 1 API test: Gọi
POST /campaignskhông có token → phải trả 401 - 1 service test: Test CampaignService.send() với mock repository
Từ khóa tìm hiểu: Jest integration test, supertest Express API testing, unit test vs integration test, test pyramid là gì
Vấn đề: Sau khi gửi xong tất cả email, campaign luôn được đánh dấu COMPLETED dù có recipient bị FAILED.
Tại sao lại sai? Nếu gửi 100 email mà 50 cái fail, campaign vẫn hiện "COMPLETED" (hoàn thành). Người dùng nghĩ mọi thứ OK nhưng thực tế một nửa email không được gửi. Giống như shipper báo "giao hàng thành công" nhưng thực tế chỉ giao được 50/100 đơn.
Cách fix: Sau khi gửi xong, đếm số lượng FAILED. Nếu có bất kỳ email nào fail → status nên là FAILED hoặc tạo thêm status mới PARTIALLY_FAILED. Cho phép người dùng biết và retry.
Từ khóa tìm hiểu: state machine pattern, campaign status management, idempotent operations
Vấn đề: Token đăng nhập được lưu trong localStorage.
Tại sao lại sai? localStorage có thể bị đọc bởi bất kỳ JavaScript nào chạy trên trang web. Nếu có lỗ hổng XSS (như vấn đề #4), hacker chỉ cần chạy localStorage.getItem('token') là lấy được token đăng nhập của người dùng.
Cách an toàn hơn: Lưu token trong HTTP-only cookie (JavaScript không thể đọc được). Bạn đã set cookie rồi nhưng vẫn giữ localStorage — nên bỏ localStorage đi, chỉ dùng cookie.
Từ khóa tìm hiểu: localStorage vs httpOnly cookie, XSS token theft, JWT storage best practices
Vấn đề: JWT hết hạn sau 1 giờ, không có cơ chế refresh.
Tại sao lại sai? Người dùng đang soạn campaign, đúng 1 giờ thì token hết hạn → bị đá ra trang login → mất nội dung đang soạn. Trải nghiệm rất tệ.
Cách fix: Implement refresh token rotation: cấp 1 access token (ngắn hạn, 15 phút) + 1 refresh token (dài hạn, 7 ngày). Khi access token hết hạn, dùng refresh token để lấy cặp token mới. Giống như thẻ ra vào công ty (access token) hết hạn mỗi ngày, nhưng bạn có thẻ nhân viên (refresh token) để đổi thẻ mới.
Từ khóa tìm hiểu: refresh token rotation, JWT refresh token flow, access token vs refresh token
Vấn đề: Nội dung HTML của email được lưu thẳng vào database mà không lọc.
Tại sao lại sai? Đây là nguyên tắc quan trọng: không bao giờ tin dữ liệu từ client. Dù frontend có validate, hacker có thể gửi request trực tiếp đến API (dùng Postman, curl) và bypass toàn bộ frontend validation. Nếu HTML chứa script độc hại, nó được lưu vào DB và hiển thị cho mọi người xem.
Đây gọi là Stored XSS — nguy hiểm hơn Reflected XSS vì nó ảnh hưởng tất cả người dùng xem nội dung đó.
Cách fix: Dùng thư viện như sanitize-html ở phía API để lọc bỏ thẻ nguy hiểm trước khi lưu vào database. Luôn sanitize ở server side, không chỉ client side.
Từ khóa tìm hiểu: Stored XSS là gì, sanitize-html npm, input validation server side, never trust client input
Vấn đề: Toàn bộ error handling dùng console.log và console.error.
Tại sao lại sai? console.log giống như viết ghi chú trên giấy rời — khi có sự cố, bạn không thể tìm lại được. Trong production, bạn cần biết: lỗi xảy ra khi nào, ở đâu, user nào bị ảnh hưởng, request nào gây ra lỗi.
Cách fix: Dùng thư viện logging như pino hoặc winston. Chúng tạo log có cấu trúc JSON với timestamp, log level (info/warn/error), context. Kết hợp với dịch vụ như Sentry để tự động alert khi có lỗi.
Từ khóa tìm hiểu: pino logger Node.js, structured logging là gì, winston vs pino, Sentry error tracking
Vấn đề: Không có tài liệu API (Swagger/OpenAPI).
Tại sao lại quan trọng? Nếu frontend developer hoặc interviewer muốn biết API có những endpoint nào, request/response format ra sao, họ phải đọc code. Đó là trải nghiệm rất tệ. API documentation giống như menu nhà hàng — khách cần biết có món gì trước khi gọi.
Cách fix: Thêm Swagger UI bằng swagger-jsdoc + swagger-ui-express. Bạn đã viết JSDoc comments cho routes rồi — chỉ cần convert sang format OpenAPI.
Từ khóa tìm hiểu: Swagger Express tutorial, OpenAPI specification, swagger-jsdoc, API documentation best practices
Vấn đề: README không giải thích lý do đằng sau các quyết định kiến trúc.
Tại sao lại quan trọng với interviewer? Interviewer biết đây là project có giới hạn thời gian. Họ muốn biết bạn nhận thức được những hạn chế, không phải bạn không biết. Viết "tôi dùng setImmediate thay vì job queue vì giới hạn thời gian, trong production tôi sẽ dùng BullMQ" cho thấy bạn hiểu vấn đề nhưng chọn trade-off hợp lý.
Nên thêm section:
- Tại sao dùng polling thay vì WebSocket
- Tại sao dùng
setImmediatethay vì message queue - Tại sao dùng
sequelize.syncthay vì migrations - Những gì sẽ thêm nếu có thêm thời gian
Từ khóa tìm hiểu: engineering trade-offs, technical decision documentation, ADR (Architecture Decision Record)
Vấn đề: Sequelize không được cấu hình connection pool.
Tại sao lại sai? Mỗi lần query database, Sequelize phải mở 1 kết nối mới — giống như mỗi lần gọi điện phải bấm số lại. Connection pool giữ sẵn vài kết nối mở — giống như speed dial, gọi liền không cần bấm số. Không có pool, khi nhiều người dùng cùng lúc, server sẽ chậm hoặc crash vì mở quá nhiều connection.
Cách fix:
new Sequelize({
pool: {
max: 10, // tối đa 10 connection
min: 2, // luôn giữ 2 connection sẵn
idle: 10000, // đóng connection nếu không dùng sau 10s
}
});Từ khóa tìm hiểu: database connection pool là gì, Sequelize pool configuration, connection pooling explained
| # | Việc cần làm | Thời gian | Tác động |
|---|---|---|---|
| 1 | Thêm rate limiting | 15 phút | Interviewer luôn hỏi về security |
| 2 | Fix logic status campaign send | 10 phút | Lỗi logic nghiệp vụ — rất dễ bị hỏi |
| 3 | Thêm onDelete CASCADE | 5 phút | Cho thấy hiểu database relationships |
| 4 | Thêm "Trade-offs" vào README | 20 phút | Cho thấy tư duy kiến trúc |
| 5 | Thêm 2-3 test có ý nghĩa | 30 phút | Cho thấy hiểu testing |
| 6 | Sanitize HTML content | 15 phút | Cho thấy hiểu bảo mật |
| 7 | Thêm pagination | 20 phút | Cho thấy nghĩ về scalability |