CommonJS và ES Modules khác nhau thế nào?
CommonJS (require/module.exports) load đồng bộ, động; ESM (import/export) là chuẩn của ngôn ngữ, phân tích tĩnh nên hỗ trợ tree-shaking và top-level await.
EventEmitter trong Node.js hoạt động thế nào và cần lưu ý gì?
EventEmitter cài đặt mẫu observer: on/once đăng ký listener, emit gọi các listener theo thứ tự đăng ký một cách đồng bộ. Phát sự kiện error mà không có listener sẽ throw; cần gỡ listener (off) để tránh leak.
fs: sync, callback và promises khác nhau thế nào? Dùng path ra sao?
fs có ba kiểu: *Sync chặn event loop, callback và fs.promises không chặn. Trên server dùng fs/promises với async/await; chỉ dùng Sync lúc khởi động. Luôn dựng đường dẫn bằng path.join/resolve thay vì nối chuỗi.
Middleware trong Express là gì?
Middleware là hàm (req, res, next) chạy tuần tự trước handler cuối: dùng cho log, auth, parse body, xử lý lỗi. Gọi next() để chuyển tiếp, hoặc kết thúc response.
Node.js là gì? Vai trò của V8 và libuv
Node.js là runtime JavaScript chạy ngoài trình duyệt. V8 biên dịch và thực thi JS; libuv cung cấp event loop, thread pool và I/O bất đồng bộ đa nền tảng. Node bind hai thứ này lại và thêm API như fs, http, net.
package.json: dependencies, devDependencies, semver và lockfile
dependencies cần khi chạy production, devDependencies chỉ khi dev/build/test. Semver MAJOR.MINOR.PATCH: ^ cho phép nâng minor/patch, ~ chỉ patch. Lockfile ghi version chính xác đã cài để build lặp lại được; nên commit và dùng npm ci trên CI.
require() hoạt động thế nào? Module cache và resolution
require() resolve đường dẫn, đọc và bọc file trong function wrapper, thực thi một lần rồi lưu module.exports vào require.cache theo đường dẫn tuyệt đối. Lần gọi sau trả về cùng object đã cache, nên module hoạt động như singleton.
So sánh npm, Yarn và pnpm
Cả ba cài package từ registry npm. pnpm dùng content-addressable store và hard link nên nhanh, tiết kiệm đĩa và chặn phantom dependency nhờ node_modules nghiêm ngặt. Yarn Berry có Plug'n'Play và workspaces. npm là mặc định đi kèm Node.
Graceful shutdown trong Node.js (SIGTERM) làm thế nào?
Khi nhận SIGTERM: ngừng nhận kết nối mới (server.close), chờ request đang xử lý xong, đóng DB/queue, rồi thoát; đặt timeout cưỡng bức để không treo. Nhờ vậy deploy/scale-down không làm rớt request.
Hash password trong Node.js: bcrypt/argon2 và timing attack
Lưu password bằng hàm hash chậm có salt riêng (argon2id hoặc bcrypt), không dùng SHA/MD5 thuần. So sánh secret/token bằng crypto.timingSafeEqual để tránh timing attack. Hash chạy trên thread pool của libuv nên dùng bản async.
Node.js đơn luồng nhưng xử lý được nhiều request nhờ đâu?
Node chạy JS trên 1 luồng nhưng giao I/O (file, mạng, DB) cho libuv/OS xử lý bất đồng bộ; khi xong, callback được đưa lại event loop. Tác vụ CPU nặng sẽ chặn toàn bộ.
process.nextTick, Promise và setImmediate khác nhau thế nào?
Sau thao tác hiện tại, Node chạy hàng đợi process.nextTick trước, rồi microtask của Promise, sau đó mới tới các pha event loop; setImmediate chạy ở pha check, setTimeout ở pha timers. nextTick đệ quy có thể làm đói I/O.
Structured logging với pino và gắn request id xuyên suốt
Log dạng JSON có cấu trúc (pino) để máy parse được; mỗi request gắn một request id (lấy từ header x-request-id hoặc sinh mới) và đưa vào child logger. Nhờ vậy truy vết mọi dòng log của một request qua nhiều service. Không log secret, dùng redact.
Test API Express với Jest và Supertest, mock dependency thế nào?
Tách app khỏi app.listen để Supertest gọi trực tiếp app mà không mở port. Mock tầng ngoài (DB, HTTP bên thứ ba) bằng jest.mock hoặc truyền dependency vào, kiểm tra status, body và side effect. Phân tầng: unit nhiều, integration với DB thật (container) vừa phải.
Thiết kế REST API: status code, idempotency, pagination
REST dùng danh từ số nhiều cho resource, method đúng ngữ nghĩa và status code chính xác (201, 204, 400, 401, 403, 404, 409, 422, 429). GET/PUT/DELETE idempotent, POST thì không (dùng Idempotency-Key). Phân trang bằng offset hoặc cursor; cursor ổn định hơn với dữ liệu lớn.
Upload file trong Node.js: multer, giới hạn và streaming lên storage
multer parse multipart/form-data. Luôn đặt limits (fileSize, files), kiểm tra mimetype/magic bytes và không tin tên file client. Với file lớn dùng diskStorage hoặc stream thẳng lên S3 thay vì memoryStorage để tránh tốn RAM; tốt nhất dùng presigned URL cho client upload trực tiếp.
Validate input trong Express: vì sao cần và dùng zod như thế nào?
Không bao giờ tin req.body/query/params. Dùng schema (zod/joi) validate ở biên, từ chối sớm bằng 400 và nhận về dữ liệu đã parse đúng kiểu. zod suy ra TypeScript type từ schema, nên một nguồn sự thật cho cả runtime và compile time.
Xử lý lỗi trong Node.js: uncaughtException và unhandledRejection
uncaughtException và unhandledRejection là lưới an toàn cuối, không phải cách xử lý lỗi bình thường. Sau khi bắt, nên log rồi tắt process có kiểm soát (để PM2/K8s khởi động lại) vì trạng thái có thể đã hỏng. Từ Node 15, unhandled rejection mặc định làm crash process.
AsyncLocalStorage dùng để làm gì? Truyền context request không cần tham số
AsyncLocalStorage (async_hooks) giữ một store gắn với chuỗi thực thi bất đồng bộ, như thread-local của Node. Dùng để mang request id, user, tenant, transaction qua mọi await/callback mà không truyền tham số. Có chi phí nhỏ và store mất nếu bị vỡ context (một số thư viện dùng pool/queue).
Các pha (phases) của event loop trong libuv hoạt động ra sao?
Mỗi vòng lặp libuv đi qua: timers, pending callbacks, idle/prepare, poll (chờ và xử lý I/O), check (setImmediate), close callbacks. Giữa các callback, Node xả hết hàng đợi microtask (nextTick rồi Promise).
Cấu hình 12-factor và scale ứng dụng Node.js (PM2, cluster, stateless)
12-factor: config qua biến môi trường, log ra stdout, process stateless và disposable (khởi động nhanh, tắt gọn). Để scale, chạy nhiều instance sau load balancer (PM2 cluster mode hoặc container) và đẩy state (session, cache, file) ra Redis/DB/object storage.
Connection pooling database trong Node.js: cỡ pool, leak và nhiều instance
Mở kết nối DB tốn kém nên dùng pool tái sử dụng. Cỡ pool nên nhỏ (vài chục) vì DB bị giới hạn max_connections; tổng = pool size x số instance phải thấp hơn giới hạn đó. Luôn release connection (finally), tránh giữ connection qua thao tác chậm, và dùng PgBouncer khi scale lớn.
HTTP keep-alive và các timeout của Node.js server: sai cấu hình gây 502 thế nào?
Node server đóng kết nối idle sau keepAliveTimeout (mặc định 5s). Nếu load balancer (ALB, nginx) có idle timeout dài hơn, nó tái sử dụng kết nối vừa bị Node đóng và sinh lỗi 502/ECONNRESET. Đặt keepAliveTimeout lớn hơn idle timeout của LB và headersTimeout lớn hơn keepAliveTimeout.
JWT vs session và bảo mật API Node.js (CORS, helmet, rate limit, SQL injection)
Session lưu state ở server (dễ thu hồi, cần store chung như Redis); JWT tự chứa claim, stateless nhưng khó thu hồi trước hạn. Bảo mật cơ bản: cookie httpOnly/secure/sameSite, CORS whitelist, helmet đặt header, rate limit chống brute force, parameterized query chống SQL injection.
Memory leak trong Node.js: nguyên nhân phổ biến và cách debug?
Leak xảy ra khi object còn được tham chiếu nên GC không thu hồi: cache/Map không giới hạn, listener không gỡ, timer, closure giữ biến lớn, biến global. Debug bằng heap snapshot (--inspect + Chrome DevTools), so sánh nhiều snapshot và theo dõi process.memoryUsage().
Scale Socket.IO/WebSocket nhiều instance: sticky session và Redis adapter
WebSocket là kết nối có trạng thái gắn với một instance. Khi scale ngang cần sticky session (nếu có HTTP long-polling) để handshake về cùng node, và Redis adapter (pub/sub) để broadcast/emit tới room vượt qua các instance. Cần tính cả reconnect, backpressure và giới hạn file descriptor.
Streams, backpressure và Buffer trong Node.js
Stream xử lý dữ liệu theo từng chunk nên dùng ít bộ nhớ với file/response lớn. Backpressure là cơ chế báo bên ghi chậm để bên đọc tạm dừng (write() trả false, chờ drain); pipeline() xử lý việc này và dọn lỗi. Buffer là vùng nhớ nhị phân kích thước cố định.
worker_threads, cluster và child_process khác nhau thế nào?
worker_threads: thread trong cùng process, có V8 isolate riêng, chia sẻ memory qua SharedArrayBuffer, hợp tác vụ CPU nặng. cluster: nhiều process Node cùng lắng nghe một port để tận dụng nhiều core. child_process: chạy process/chương trình bên ngoài, giao tiếp qua IPC hoặc stdio.