Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
Node.js
Node.js chạy JS trên server nhờ V8 engine + libuv async I/O — không có DOM/window nhưng có fs/http/crypto; single-threaded event loop xử lý hàng nghìn concurrent connections hiệu quả.
- Node.js là runtime environment cho phép chạy JavaScript trên server, được xây dựng trên V8 engine của Chrome và thư viện C++ libuv xử lý async I/O.
- Khác biệt cốt lõi với browser JS: không có DOM, window, document, localStorage — thay vào đó có
fs(đọc/ghi file),http(tạo server),path,os,cryptovà hàng nghìn npm packages.
Ví dụ thực tế: const fs = require('fs'); fs.readFile('data.json', 'utf8', (err, data) => console.log(data)) — đây là code không thể chạy trong browser.
- Node.js dùng event loop single-threaded nên xử lý hàng nghìn concurrent connections hiệu quả với I/O-heavy workloads như API server, real-time chat.
- Không phù hợp cho CPU-intensive tasks như video encoding vì block event loop.
V8 dùng JIT compilation biên dịch JS thành native machine code — hidden class optimization cho phép objects cùng shape dùng chung internal class và chạy nhanh hơn.
- V8 là JavaScript engine mã nguồn mở của Google viết bằng C++, được dùng trong Chrome và là trái tim của Node.js.
- Điểm khác biệt quan trọng: V8 không interpret JS từng dòng mà dùng JIT (Just-In-Time) compilation — biên dịch JS thành native machine code ngay lúc chạy, cho tốc độ nhanh gần bằng C++.
- Node.js nhúng V8 vào C++ runtime và bổ sung thêm libuv (async I/O), các built-in modules (fs, http, crypto...) để tạo thành môi trường chạy JS hoàn chỉnh ngoài browser.
- Thực tế ảnh hưởng đến dev: V8 có hidden class optimization — object có cùng shape (thứ tự properties) sẽ dùng chung internal class và chạy nhanh hơn, nên tránh thêm properties động vào object sau khi tạo trong hot paths.
Node.js phản ứng với events thay vì chạy tuần tự — đăng ký callback, tiếp tục xử lý việc khác, callback được gọi khi I/O hoàn thành; block khi chạy heavy sync computation.
- Event-driven architecture nghĩa là code phản ứng với events thay vì chạy tuần tự từ trên xuống.
- Trong Node.js, thay vì blocking chờ đợi I/O xong, bạn đăng ký callback và Node.js tiếp tục xử lý việc khác — khi I/O hoàn thành, event được emit và callback được gọi.
Ví dụ cụ thể: server.on('request', (req, res) => {...}) — server không blocking chờ từng request mà lắng nghe event 'request' liên tục.
- Điều này cho phép một thread đơn xử lý hàng nghìn connections đồng thời vì hầu hết thời gian Node.js không làm gì cả, chỉ chờ I/O.
Pitfall: nếu bạn chạy heavy computation đồng bộ (vòng lặp triệu lần), event loop bị block và mọi requests khác phải chờ — đây là lý do Node.js không phù hợp cho CPU-intensive work.
Non-blocking I/O cho phép Node.js xử lý hàng nghìn connections trên 1 thread — tránh readFileSync/JSON.parse(largeData)/vòng lặp nặng vì chúng block event loop và làm tất cả requests khác phải chờ.
- Non-blocking I/O hoạt động nhờ libuv delegate I/O operations cho OS kernel hoặc thread pool: khi gọi
fs.readFile(), Node.js đăng ký callback rồi trả control về event loop ngay lập tức, OS xử lý I/O ở background, khi xong thêm callback vào event queue để event loop xử lý. - So với blocking model Apache (thread-per-request): Apache cấp 1 thread/request, thread block khi chờ DB query — 1000 concurrent requests cần 1000 threads (~1GB RAM).
- Node.js single-threaded xử lý 1000 requests trên 1 thread vì hầu hết thời gian chờ I/O là idle.
- Throughput thực tế: Node.js thường đạt 10k-50k req/s cho I/O-heavy API, so với Apache ~1k-5k req/s.
- Khi nào blocking xảy ra accidentaly:
fs.readFileSync(),JSON.parse(largeData), vòng lặp tính toán nặng,crypto.pbkdf2Sync()— tất cả đều block event loop và làm tất cả requests khác phải chờ. - Solution: Worker Threads cho CPU-intensive, luôn dùng async variants.
dependencies cho production; devDependencies cho build/test tools (không được bundle vào production) — npm ci trong CI/CD đảm bảo deterministic install từ lock file. npm (Node Package Manager) là công cụ quản lý packages mặc định đi kèm Node.js, với registry hơn 2 triệu packages.
- Phân biệt quan trọng:
npm install reactcài vàodependencies— những gì cần thiết để app chạy trên production.npm install --save-dev jest typescript eslintcài vàodevDependencies— chỉ cần trong quá trình development, không được bundle vào production build.
Ví dụ thực tế: khi deploy lên server, chạy npm install --production sẽ bỏ qua devDependencies, giảm đáng kể dung lượng node_modules.
Pitfall hay gặp: cài nhầm package production vào devDependencies (app crash trên server) hoặc ngược lại (bloat production bundle).
- Ngoài npm còn có pnpm (nhanh hơn, tiết kiệm disk) và yarn đang được dùng phổ biến.
package.json manifest chứa scripts (lifecycle hooks), dependencies/devDependencies, engines (Node version), exports (conditional entry points), và workspaces (monorepo). package.json là manifest file của Node.js project. scripts hỗ trợ lifecycle hooks: preinstall/postinstall chạy trước/sau npm install, prebuild/postbuild bao quanh build — dùng để codegen, copy assets. engines: { node: '>=18.0.0' } báo CI/CD và người dùng version Node cần thiết, npm cảnh báo nếu không match.
Entry points: main (CJS fallback), module (ESM cho bundlers hỗ trợ), exports (field mới nhất — conditional exports theo environment): { '.': { import: './dist/index.mjs', require: './dist/index.cjs' } }. type: 'module' đặt default module system là ESM cho tất cả .js files. workspaces: ['packages/*'] cho monorepo — npm install một lần, symlink packages. peerDependencies khai báo packages host app phải cung cấp (tránh duplicate React trong component libraries).
package-lock.json lưu exact versions + integrity hashes (sha512) của tất cả packages kể cả transitive dependencies — đảm bảo install reproducible trên mọi máy/CI. npm install cập nhật lock file nếu package.json thay đổi; npm ci (clean install) xóa node_modules rồi install chính xác từ lock file, không bao giờ update lock — dùng trong CI/CD để đảm bảo deterministic builds.
- Khi nào xóa và regenerate: khi lock file bị corrupt, sau major Node.js upgrade, hoặc khi muốn update tất cả dependencies lên latest compatible.
- Integrity field:
'integrity': 'sha512-abc...'— npm verify hash sau download, phát hiện supply chain attacks (tampered packages). - Tương đương:
yarn.lock(Yarn),pnpm-lock.yaml(pnpm) — cùng mục đích nhưng format khác nhau. - Quan trọng: KHÔNG commit lock file của library packages lên npm registry (chỉ commit lock của applications).
CommonJS (CJS): require() load synchronous và blocking — có thể require() ở bất kỳ đâu trong code kể cả trong if/function, module.exports là bất kỳ giá trị gì, không hỗ trợ tree-shaking.
- ESM:
importphải ở top-level (static analysis), load asynchronous cho phép parallel fetch, tree-shakeable vì bundlers biết chính xác exports nào được dùng. - ESM exclusive features: top-level
await, import.meta.url, dynamicimport(). - Interop challenges: CJS có thể
require()ESM file? Không được, phải dùng dynamicimport(). - ESM có thể import CJS? Có, nhưng chỉ default import.
- Dual package hazard: publish cả CJS lẫn ESM cùng package — nếu app load cả hai, sẽ có 2 instances của module (state, class không share được).
- Giải pháp: conditional exports trong package.json.
type: 'module'trong package.json = tất cả.jslà ESM, CJS phải đổi thành.cjs. - Node.js 22+ hỗ trợ
--experimental-require-moduleđể require() ESM.
Module http là built-in, không cần cài thêm: const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); }); server.listen(3000). req chứa method, url, headers và là Readable stream (đọc body qua req.on('data', chunk => ...)). res là Writable stream — phải gọi res.end() để kết thúc, không thì client treo mãi.
- Trong thực tế hiếm ai dùng
httpmodule trực tiếp vì rất verbose — Express, Fastify, Hono wrap lại với routing, middleware, body parsing tiện hơn nhiều. - Tuy nhiên hiểu
httpmodule giúp debug networking issues và hiểu cách các framework hoạt động bên dưới.
Express là web framework minimal cho Node.js, cung cấp routing và middleware system mà không áp đặt cấu trúc dự án.
- Phổ biến nhất trong Node.js ecosystem vì: API cực đơn giản (
app.get('/users', handler)), middleware pipeline linh hoạt cho phép compose nhiều concerns (auth, logging, validation) thành chuỗi, và hệ sinh thái packages khổng lồ nhưcors,helmet,multer,passport. - Express cũng là nền tảng để nhiều framework khác xây dựng lên như NestJS.
- Tuy nhiên nhược điểm là quá minimal — không có validation, ORM, hay structure built-in, dev phải tự lắp ghép.
- Với TypeScript projects hiện đại, nhiều team chuyển sang NestJS (opinionated, decorator-based) hoặc Fastify (nhanh hơn 2-3x, schema validation built-in).
Middleware là function signature (req, res, next) thực thi tuần tự theo thứ tự đăng ký — thứ tự là tuyệt đối.
- App-level:
app.use(helmet())áp dụng cho toàn bộ app. - Router-level:
router.use(authMiddleware)chỉ cho nhóm routes. - Error-handling middleware có 4 tham số
(err, req, res, next)phải đăng ký SAU tất cả routes. - Third-party ecosystem:
helmet(security headers),morgan(logging),compression(gzip),passport(auth strategies). - Flow:
next()chuyển sang middleware tiếp theo,next(err)nhảy thẳng tới error handler bỏ qua tất cả non-error middleware, không gọinext()và khôngres.send()thì request treo mãi.
Pitfall: đặt express.json() sau route handler → req.body undefined; đặt cors() sau route → preflight OPTIONS request trả 404.
Environment variables tách config khỏi code — 12-factor app methodology: config là bất kỳ thứ gì thay đổi giữa environments (dev/staging/prod).
- Cấu trúc .env file:
DATABASE_URL=postgresql://...,JWT_SECRET=...,NODE_ENV=development. dotenv:require('dotenv').config()load .env vào process.env, KHÔNG override existing env vars — production inject trực tiếp qua Docker/K8s/CI không cần .env file. dotenv-safe: kiểm tra required vars từ.env.exampletại startup, throw error nếu thiếu thay vì silent undefined. - Config validation với Zod:
const config = z.object({ DATABASE_URL: z.string().url(), PORT: z.coerce.number().default(3000), JWT_SECRET: z.string().min(32) }).parse(process.env)— fail fast với error rõ ràng. - Tạo
config.tsmodule export typed config thay vì accessprocess.envtrực tiếp khắp nơi — dễ test và type-safe.
Pitfall: process.env.PORT luôn là string, cần coerce sang number; commit .env vào git — dùng .gitignore và .env.example làm template.
Node.js là runtime JavaScript trên server, dùng V8 engine.
- Ưu điểm: non-blocking I/O xử lý nhiều requests đồng thời, cùng ngôn ngữ với frontend (JS) nên team dùng chung tooling và type definitions, npm ecosystem khổng lồ với hàng triệu packages, fit cho real-time apps (chat, notifications) nhờ event-driven model.
- Thường dùng cho API servers, microservices, BFF (Backend for Frontend) — không phù hợp CPU-intensive workloads (video encoding, ML inference) vì single-threaded event loop bị block.
process là global object không cần require. process.env: chứa environment variables — KHÔNG bao giờ log toàn bộ process.env vì chứa secrets; validate với zod/envalid ngay startup để fail fast nếu thiếu. process.argv: array [nodePath, scriptPath, ...userArgs] — dùng yargs hoặc commander để parse thay vì thủ công.
Signal handling: process.on('SIGTERM', gracefulShutdown) — xử lý khi container/PM2 dừng process (close DB connections, finish in-flight requests); SIGINT = Ctrl+C. process.memoryUsage() trả về { rss, heapTotal, heapUsed, external } — monitor memory leaks bằng cách log định kỳ. process.hrtime.bigint() cho high-resolution timestamps (nanoseconds) để benchmark code. process.exit(1) exit với error code — quan trọng cho CI pipelines biết lệnh fail. process.on('uncaughtException') và process.on('unhandledRejection') — log error rồi process.exit(1), không recover vì process state có thể corrupt.
__dirname resolve tuyệt đối từ file, không phải từ process.cwd() — trong ESM dùng fileURLToPath(import.meta.url) thay thế vì __dirname không tồn tại. __dirname cho đường dẫn tuyệt đối đến thư mục chứa file đang chạy, __filename cho đường dẫn đến chính file đó.
Ví dụ thực tế: path.join(__dirname, 'templates', 'email.html') để đọc file template bất kể app được chạy từ thư mục nào — nếu dùng đường dẫn tương đối './templates/email.html' sẽ resolve từ process.cwd() (thư mục làm việc hiện tại) và có thể sai khi chạy từ nơi khác.
Pitfall quan trọng với ES Modules: __dirname và __filename không tồn tại trong file .mjs hoặc khi type: module trong package.json.
- Thay thế:
import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename).
Node.js event loop xử lý async ops theo phases: timers→poll→check — giữa mỗi phase flush toàn bộ microtask queue (nextTick → Promises); thứ tự: process.nextTick(C), Promise.then(B), setTimeout(A, 0) → chạy C, B, A.
Event loop là vòng lặp liên tục xử lý callbacks theo 6 phases:
- Timers: thực thi callbacks của
setTimeout/setIntervalđã đến hạn; - Pending callbacks: I/O errors từ vòng trước;
- Idle/prepare: internal use;
- Poll: fetch I/O events mới — nếu queue empty và không có timers pending, block tại đây chờ I/O;
- Check:
setImmediatecallbacks; - Close callbacks:
socket.on('close')
Giữa MỖI phase, Node.js xử lý toàn bộ microtask queue: Promise callbacks (then/catch) + queueMicrotask() + process.nextTick() (nextTick ưu tiên hơn Promise).
Starvation: nếu microtask queue không bao giờ empty (Promise resolve tạo Promise mới), event loop không bao giờ qua phase tiếp theo.
Ví dụ thực tế execution order: setTimeout(A, 0) → Promise.resolve().then(B) → process.nextTick(C) — thứ tự chạy: C, B, A.
Trong I/O callback context, setImmediate() LUÔN chạy trước setTimeout(fn, 0) — vì check phase đến trước timers phase sau poll; bên ngoài I/O context thứ tự không đảm bảo. setTimeout(fn, 0) thực ra là setTimeout(fn, 1) minimum — chạy trong timers phase nếu timer đã expired. setImmediate() chạy trong check phase (ngay sau poll phase).
- Ngoài I/O context (ví dụ main script): thứ tự giữa hai cái không đảm bảo — phụ thuộc vào system clock resolution và thời gian setup event loop.
- Trong I/O callback context: setImmediate() LUÔN chạy trước setTimeout(fn, 0) — vì I/O callback chạy ở poll phase, sau poll là check phase (setImmediate), sau đó mới quay lại timers phase.
Ví dụ: fs.readFile(file, () => { setImmediate(() => console.log('immediate')); setTimeout(() => console.log('timeout'), 0); }) — luôn in 'immediate' trước 'timeout'.
- Practical implication: khi muốn chạy code sau I/O operation hiện tại mà trước timers, dùng setImmediate.
Dùng async/await để giải quyết callback hell — code đọc như synchronous, try/catch natural; Promise.all() cho parallel thay vì await trong for loop.
- Callback hell (pyramid of doom) — ví dụ thực:
fs.readFile('a.txt', (err, data) => { if(err) throw err; db.query(data, (err, rows) => { if(err) throw err; sendEmail(rows[0], (err) => { if(err) throw err; console.log('done') }) }) })— 3 cấp lồng nhau, error handling lặp lại, logic khó theo dõi. - Giải pháp Promise chaining:
readFile('a.txt').then(data => db.query(data)).then(rows => sendEmail(rows[0])).catch(err => console.error(err))— flat hơn nhưng còn awkward với multi-value. - Giải pháp async/await:
try { const data = await readFile('a.txt'); const rows = await db.query(data); await sendEmail(rows[0]); } catch(err) { console.error(err); }— đọc như synchronous code, try/catch natural. - Error propagation khác nhau: callback phải check
errmỗi cấp thủ công, một missed check = silent failure; Promise/async throw tự động bubble up đến .catch/try-catch.
Pitfall async/await: await trong vòng lặp for...of sẽ chạy sequential — dùng Promise.all(items.map(item => process(item))) để parallel.
Promise là object đại diện cho async operation, có 3 states không thể revert: pending → fulfilled/rejected.
- Microtask queue:
.then()callbacks được đặt vào microtask queue (ưu tiên cao hơn setTimeout), chạy sau synchronous code nhưng trước event loop phases. - Static methods quan trọng:
Promise.all([p1,p2])— chờ tất cả resolve, reject ngay khi 1 cái reject (fail-fast);Promise.allSettled([p1,p2])— chờ tất cả settle bất kể kết quả, trả về[{status, value/reason}]— dùng khi muốn biết kết quả từng cái;Promise.race([p1,p2])— resolve/reject theo cái đầu tiên settle (dùng cho timeout pattern);Promise.any([p1,p2])— resolve khi 1 cái fulfilled, reject chỉ khi tất cả reject. - Error handling chain: nếu không có
.catch(), unhandled rejection — Node.js 15+ crash process mặc định.Promise.resolve(value)tạo fulfilled promise ngay, hữu ích để normalize sync/async APIs.
Pitfall: error swallowing trong .then(onFulfilled) không có .catch() — luôn chain .catch() hoặc dùng try/catch với await.
async/await là syntactic sugar trên Promise — async function luôn return Promise, await pause execution của function đó (không block thread) cho đến khi Promise settle.
- Error handling: try/catch bắt rejected Promise như exception thông thường —
try { const data = await fetch(url).then(r => r.json()); } catch(e) { / network error, JSON parse error / }. - Parallel execution: sequential
await a(); await b()tổng thời gian = a + b; parallelconst [ra, rb] = await Promise.all([a(), b()])tổng thời gian = max(a, b).
Pitfall #1 — sequential await trong loop: for (const id of ids) { await fetchUser(id); } chạy tuần tự, chậm.
- Fix:
await Promise.all(ids.map(id => fetchUser(id))).
Pitfall #2 — error không được handle: async function foo() { await riskyOp(); } gọi foo() mà không await/catch = unhandled rejection.
- Top-level await: ESM modules hỗ trợ
awaitở top level (ngoài function) — hữu ích cho dynamic imports, DB init. - Khi debug: stack traces của async/await rõ ràng hơn Promise chains nhiều.
Buffer là class built-in xử lý binary data — vùng nhớ raw bytes nằm ngoài V8 heap.
- JavaScript thuần không có kiểu dữ liệu binary, Buffer lấp đầy khoảng trống này cho Node.js.
- Cần dùng khi: đọc file binary (ảnh, PDF, executable), xử lý network packets TCP/UDP, mã hóa/giải mã base64 (
Buffer.from('hello').toString('base64')), tính hash vớicrypto.createHash.
Ví dụ thực tế: upload ảnh qua API, req.body là Buffer chứa raw bytes của file, cần convert hoặc pipe trực tiếp lên S3.
Pitfall: Buffer.allocUnsafe(size) nhanh hơn nhưng chứa dữ liệu cũ trong bộ nhớ — chỉ dùng khi sẽ ghi đè toàn bộ ngay sau đó, dùng Buffer.alloc(size) (zero-filled) cho trường hợp thông thường.
express.Router() tạo mini-app với routes và middleware riêng biệt — giải pháp chính để tổ chức code theo feature.
- Mounting pattern:
app.use('/api/v1/users', userRouter)prefix tất cả routes trong router. - Route-level middleware:
router.use(authMiddleware)chỉ áp dụng cho routes trong router đó. - Nested routers:
adminRouter.use('/users', adminUserRouter)để nest sâu hơn. - Versioned API pattern:
app.use('/api/v1', v1Router); app.use('/api/v2', v2Router)— v1 và v2 hoàn toàn độc lập, dễ deprecate. - Cấu trúc thực tế:
routes/users.tsexport router,routes/index.tsaggregate tất cả routers,app.tschỉapp.use('/api', mainRouter).
Pitfall: router.use(middleware) sau router.get(...) thì route đó không được áp dụng middleware — phải đặt middleware TRƯỚC routes.
Express map trực tiếp tới HTTP methods với semantics rõ ràng: GET (idempotent, safe — chỉ đọc, không side effects), POST (không idempotent — tạo resource mới, mỗi call tạo record mới), PUT (idempotent — replace toàn bộ resource, gửi thiếu field thì field đó bị null/default), PATCH (idempotent — partial update, chỉ gửi fields cần thay đổi), DELETE (idempotent — xóa, gọi nhiều lần kết quả như nhau).
- CRUD thực tế:
GET /users(list),POST /users(create, trả 201),GET /users/:id(read),PUT /users/:id(replace, trả 200),PATCH /users/:id(update, trả 200),DELETE /users/:id(delete, trả 204 no content). - Status codes quan trọng: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 500 Internal Server Error.
- Content negotiation:
req.accepts('json')kiểm tra client Accept header.
Pitfall: dùng GET với body để filter — không đúng semantics, dùng query params thay thế.
Error handling middleware có 4 tham số (err, req, res, next) — phải đăng ký CUỐI CÙNG sau tất cả routes.
- Pattern tốt nhất: tạo custom AppError class
class AppError extends Error { constructor(public statusCode: number, message: string, public isOperational = true) { super(message) } }— phân biệt operational errors (404, validation fail — dự đoán được) vs programming errors (null reference — bugs). - Async error wrapper để tránh try/catch lặp lại:
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next). - Centralized error handler kiểm tra
err.isOperational: nếu true thì gửi message cho client, nếu false thì log và trả 500 generic. - Express 5 async routes tự động forward error nên không cần wrapper nữa.
Pitfall: quên next tham số thứ 4 → Express không nhận ra là error handler.
- Không gọi
next()saures.json()trong error handler → double response error.
CORS là cơ chế browser bảo vệ user — server phải opt-in cho phép cross-origin requests.
Preflight mechanism: browser tự động gửi OPTIONS request trước khi gửi request thật nếu là preflighted request (method không phải GET/POST/HEAD, hoặc header không phải simple header như Content-Type: application/json).
Simple requests (GET với simple headers) không cần preflight.
Cấu hình production đúng: cors({ origin: ['https://app.example.com'], methods: ['GET','POST','PUT','PATCH','DELETE'], allowedHeaders: ['Content-Type','Authorization'], credentials: true }) — credentials: true bắt buộc khi gửi cookies/Authorization header.
Quan trọng: khi credentials: true, origin KHÔNG được là * — phải liệt kê explicit.
Debug tips:
- kiểm tra response header
Access-Control-Allow-Origintrong DevTools Network tab, credentials: truenhưng server trả*→ browser chặn,- đặt
cors()TRƯỚC routes để OPTIONS preflight được handle
Pitfall: CORS là browser enforcement — Postman/curl không bị chặn, chỉ browser mới bị.
express.json() parse Content-Type application/json thành req.body object — built-in từ Express 4.16+, không cần body-parser riêng. express.urlencoded({ extended: true }) parse HTML form data (application/x-www-form-urlencoded); extended: true dùng qs library cho phép nested objects, extended: false dùng querystring chỉ flat.
- Size limit mặc định 100kb — thay đổi:
express.json({ limit: '10mb' }). - Security: large payloads gây DoS — luôn set limit hợp lý, không để mặc định cho upload endpoint.
- Custom content types:
express.json({ type: 'application/vnd.api+json' }). - Raw parser:
express.raw({ type: 'application/octet-stream' })cho binary. - Text parser:
express.text()cho plain text webhooks.
Pitfall: express.json() không parse multipart/form-data (file upload) — cần multer riêng.
- Nếu
req.bodylà undefined, kiểm tra middleware đã được đăng ký chưa và đúng thứ tự chưa.
Rate limiting bảo vệ API khỏi spam, brute-force và DDoS.
- Fixed window: đếm requests trong window cố định (0:00-0:15) — dễ bypass bằng cách gửi burst ở cuối window cũ + đầu window mới.
- Sliding window: window trượt theo thời gian thực, chính xác hơn nhưng cần Redis.
express-rate-limitdùng fixed window:rateLimit({ windowMs: 15601000, max: 100, standardHeaders: true, legacyHeaders: false }). - Distributed rate limiting với Redis:
rate-limit-redisstore để share state giữa nhiều server instances — bắt buộc trong cluster/multi-server deployment, nếu dùng in-memory thì mỗi instance có counter riêng, giới hạn thực tế làmax * numInstances. - Rate limit headers trả về client:
RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset(standardHeaders: true). - Limits khác nhau per endpoint: auth endpoints (5 req/15min), API endpoints (100 req/15min), public endpoints (1000 req/15min).
- Per-user limits:
keyGenerator: (req) => req.user?.id || req.ip— authenticated users có limit riêng theo userId thay vì IP.
Pitfall: behind reverse proxy thì req.ip là IP của proxy — cần app.set('trust proxy', 1) để lấy IP thật từ X-Forwarded-For.
JWT gồm header.payload.signature (base64url-encoded) — stateless, không cần lưu session; nhưng không thể revoke trước hạn nên access token phải ngắn (15 phút) kết hợp refresh token.
- JWT là chuẩn mở (RFC 7519) để truyền thông tin an toàn dưới dạng JSON, được ký số để đảm bảo tính toàn vẹn.
- Gồm 3 phần ngăn cách bởi dấu chấm, mỗi phần base64url-encoded: Header (
{alg: 'HS256', typ: 'JWT'}), Payload (claims như{sub: userId, exp: timestamp, role: 'admin'}), Signature (HMAC của header+payload bằng secret key). - Ưu điểm stateless: server không cần lưu session, chỉ cần verify signature — phù hợp microservices và horizontal scaling.
- Nhược điểm quan trọng: không thể revoke JWT trước khi hết hạn (không có centralized blacklist), nên access token phải có
expngắn (15 phút) kết hợp refresh token.
Pitfall phổ biến nhất: lưu JWT trong localStorage thay vì httpOnly cookie — dễ bị XSS đánh cắp.
Lưu access token trong response body, refresh token trong httpOnly cookie — phải validate alg header (reject alg:none) và implement rotation (invalidate old refresh token on each use).
- Full auth flow: POST /login → verify credentials →
jwt.sign({ sub: user.id, role: user.role }, secret, { expiresIn: '15m' })tạo access token +jwt.sign({ sub: user.id }, refreshSecret, { expiresIn: '7d' })tạo refresh token → trả access token trong response body, refresh token trong httpOnly cookie (Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=Strict). - Token storage: httpOnly cookie bảo vệ khỏi XSS (JS không đọc được), nhưng cần CSRF protection; localStorage tiện hơn nhưng XSS đánh cắp được — httpOnly cookie là best practice cho web apps.
- Auth middleware:
const token = req.headers.authorization?.split(' ')[1]; const payload = jwt.verify(token, secret); req.user = payload. - Refresh token rotation: mỗi lần dùng refresh token thì issue cặp mới (access + refresh) và invalidate refresh token cũ — lưu refresh token hash trong DB để có thể revoke.
- POST /refresh nhận cookie → verify → trả access token mới.
- POST /logout xóa cookie và blacklist refresh token.
Pitfall: không validate alg trong header → attacker đổi sang alg: none; dùng { algorithms: ['HS256'] } trong verify options.
bcrypt được thiết kế đặc biệt cho password hashing — khác hoàn toàn SHA-256/MD5 vốn được tối ưu để NHANH (SHA-256 hash 10 tỷ lần/giây trên GPU, bcrypt cost=12 chỉ ~250 lần/giây).
- Adaptive cost factor:
saltRounds(cost) tăng thì thời gian tăng gấp đôi — cost=10 ~100ms, cost=12 ~400ms, cost=14 ~1.6s. - Chọn cost sao cho ~250ms trên server của bạn và tăng dần theo năm khi phần cứng mạnh lên.
- Built-in salt: bcrypt tự tạo random salt unique per password và nhúng vào hash output — tránh rainbow table attacks và đảm bảo cùng password tạo hash khác nhau mỗi lần.
- Timing attacks:
bcrypt.compare()là constant-time comparison, không dùng===để so sánh hash. - Argon2 là alternative hiện đại hơn (winner PHC 2015): argon2id kháng GPU và side-channel attacks tốt hơn, được OWASP khuyến nghị năm 2023+; dùng
argon2package trong Node.js.
Pitfall: hash password trong service layer, không trong model hook — dễ bị double-hash nếu hook chạy lại khi update field khác.
- Cache-aside (lazy loading): check cache → miss → fetch DB → write cache → return — pattern phổ biến nhất, code:
const cached = await redis.get(key); if (cached) return JSON.parse(cached); const data = await db.query(...); await redis.setex(key, 300, JSON.stringify(data)); return data. - Write-through: write cache + DB đồng thời — data luôn fresh nhưng write chậm hơn.
- Write-behind: write cache trước, async flush xuống DB — write rất nhanh nhưng risk mất data nếu cache crash.
- Cache invalidation strategies: TTL (đơn giản, chấp nhận stale data), event-based (on update → delete cache key), versioned keys (
user:${id}:v${version}). - TTL chọn sao cho: data thay đổi ít → TTL dài (1 giờ), data thay đổi nhiều → TTL ngắn (30s) hoặc invalidate on write.
- Cache warming: pre-populate cache khi startup để tránh cold start — gọi
warmCache()sau khi server start. - Thundering herd problem: nhiều requests đồng thời hit expired key → tất cả cùng query DB → DB quá tải.
- Fix: probabilistic early expiration (renew cache trước khi hết hạn), mutex lock (chỉ 1 request query DB, còn lại chờ), stale-while-revalidate.
Pitfall: cache key collision — luôn dùng namespace ${service}:${resource}:${id}.
Node.js 14 trở về trước: unhandled rejection chỉ print warning, process tiếp tục — silent failure nguy hiểm.
Node.js 15+: mặc định crash process với exit code 1 — breaking change.
Hierarchy xử lý đúng:
- try/catch trong async functions là chính,
.catch()chain cho fire-and-forget promises,process.on('unhandledRejection', (reason) => { logger.error('Unhandled rejection', reason); gracefulShutdown(1); })là safety net cuối — không phải cơ chế chính.process.on('uncaughtException', (err) => { logger.error(err); gracefulShutdown(1); })cho sync throws
Monitoring: gửi đến Sentry trước khi shutdown — Sentry.captureException(reason); await Sentry.flush(2000).
Graceful shutdown: stop accepting requests, wait for in-flight, close DB, exit.
Dùng ESLint rule @typescript-eslint/no-floating-promises để catch missing awaits lúc compile time — tốt hơn runtime detection.
fs.readFile() load toàn bộ file vào RAM trước khi callback — file 100MB chiếm ít nhất 100MB heap; 100 concurrent requests = 10GB RAM, dễ OOM crash. fs.createReadStream() đọc theo chunks (default highWaterMark 64KB) — file 100MB chỉ dùng ~64KB RAM bất kể bao nhiêu requests đồng thời.
- Serve file thực tế:
fs.createReadStream(filePath).pipe(res)stream thẳng đến TCP buffer không qua RAM. pipeline() API (Node 10+) tốt hơn pipe():await pipeline(fs.createReadStream(path), res)— tự cleanup khi error, tránh memory leak khi client disconnect. - Backpressure tự động: pipe/pipeline dừng đọc file khi client chậm, tiếp tục khi TCP buffer drain.
- Khi nào dùng readFile: config files < 1MB đọc lúc startup, JSON parse một lần.
- Khi nào dùng createReadStream: file download, log streaming, CSV export, bất kỳ file > vài MB.
readFileSync blocking — giữ event loop hoàn toàn cho đến khi OS trả data, không xử lý request nào khác trong thời gian đó. readFile non-blocking — gửi I/O request đến libuv thread pool, event loop tự do xử lý requests khác.
Impact thực tế: readFileSync đọc file 50ms trên server 1000 req/s → mỗi request xếp hàng bị delay 50ms cộng dồn → latency spike hàng giây.
Khi sync chấp nhận được:
- module initialization trước
server.listen()—const config = JSON.parse(fs.readFileSync('config.json', 'utf8'))chạy một lần duy nhất lúc startup, không có traffic; - CLI tools;
- build scripts
Quy tắc: bất kỳ code nào chạy SAU server.listen() trong request path — bắt buộc dùng async.
Đo blocking: node --prof app.js → node --prof-process isolate-*.log → readFileSync trong hot path hiện rõ ràng với % CPU time.
- Breakpoint debugging:
node --inspect app.jsexpose debugger tại port 9229; mởchrome://inspect→ attach.--inspect-brkdừng tại dòng đầu tiên — dùng khi debug initialization code. - VS Code launch.json:
{ type: 'node', request: 'launch', program: '${workspaceFolder}/src/index.ts', runtimeArgs: ['-r', 'ts-node/register'] }— breakpoints trong TypeScript với source maps. - Memory profiling: Chrome DevTools → Memory tab → Heap Snapshot → so sánh 2 snapshots tìm memory leak; retained size cho thấy objects đang giữ gì.
- CPU profiling: DevTools → Performance tab → Record → stress → flame graph, tìm hot functions.
clinic doctor -- node app.js: tự phát hiện event loop delay, I/O issues, memory problems, generate HTML report.0x app.js: interactive CPU flamegraph, hiển thị call stacks chiếm CPU. - Source maps:
sourceMap: truetrong tsconfig để debugger map JS → TS.
Pitfall: --inspect trong production mà expose port ra internet → remote code execution vulnerability; bind tới localhost only.
- SQL injection (OWASP A03):
db.query('SELECT * FROM users WHERE id = ' + req.params.id)→ attacker truyền1 OR 1=1dump toàn bộ DB. - Fix: parameterized queries
db.query('SELECT * FROM users WHERE id = $1', [id])hoặc ORM. - NoSQL injection MongoDB:
{ username: req.body.username }với body{ username: { $gt: '' } }bypass auth. - Fix: validate với Zod/Joi trước khi query.
- XSS (A07): lưu
<script>alert(1)</script>vào DB rồi render unescaped → steal cookies. - Fix: escape output, CSP headers, DOMPurify.
- ReDoS:
/(a+)+/.test(userInput)với input'aaaaaaaaab'→ exponential backtracking block event loop hàng giây. - Fix:
safe-regexpackage kiểm tra pattern, timeout regex execution. - Directory traversal:
fs.readFile('/uploads/' + req.query.file)vớifile=../../etc/passwd. - Fix:
path.resolve()+ verify kết quả starts with allowed directory. - Prototype pollution:
obj[userKey] = userValuevới key__proto__corrupt Object prototype. - Fix:
Object.create(null)cho plain objects, validate keys. - Dependency vulnerabilities:
npm audit+npm audit fix,snykcho CI/CD. - Helmet.js: một lệnh
app.use(helmet())set 11 security headers (X-Frame-Options, HSTS, CSP cơ bản, nosniff).
- Test pyramid cho Node.js API: unit tests (service/utils logic) → integration tests (controller + real DB) → e2e tests (full HTTP).
- Controller testing với supertest:
const res = await request(app).post('/users').send({ email: 'test@test.com' }); expect(res.status).toBe(201)— không cần start server, supertest inject request trực tiếp. - Service testing: mock DB layer với
jest.mock('../db')→(db.findUser as jest.Mock).mockResolvedValue({ id: 1 })— test business logic độc lập với DB. - Middleware testing: gọi middleware với mock
req,res,nextobjects — verifynext()được gọi hayres.status()được set. - Integration tests: dùng test DB riêng (
TEST_DATABASE_URL), chạy migrations trước test suite, truncate tables giữa tests (afterEach). - Mock strategies:
jest.mock()cho modules,jest.spyOn()cho methods,jest.useFakeTimers()cho time-dependent code. - Test database setup:
globalSetupchạy migration một lần,beforeEachtruncate data,afterAlldisconnect. - Coverage:
jest --coverage+coverageThreshold: { global: { lines: 80 } }enforce minimum.
Pitfall: không isolate tests → test order dependent failures; mock tất cả external calls (HTTP, email) để tests deterministic.
REST (Representational State Transfer) là kiến trúc thiết kế API dựa trên HTTP, trong đó mỗi URL đại diện cho một resource (tài nguyên).
- Nguyên tắc quan trọng: dùng danh từ số nhiều cho endpoints (/users, /products), dùng đúng HTTP methods — GET lấy dữ liệu, POST tạo mới, PUT/PATCH cập nhật, DELETE xóa.
- Status codes phải chính xác: 200 thành công, 201 tạo mới OK, 400 request sai, 401 chưa đăng nhập, 404 không tìm thấy, 500 lỗi server.
- Ngoài ra cần versioning (/api/v1/), pagination (?page=1&limit=20), và response format thống nhất dạng
{ data, error, meta }để frontend dễ xử lý.
Để xử lý file upload trong Express, thư viện phổ biến nhất là multer hoạt động như một middleware, cấu hình bằng const upload = multer({ dest: 'uploads/' }) rồi gắn vào route cụ thể như app.post('/upload', upload.single('file'), handler).
Khi nhận file cần validate kỹ hai thứ: kích thước file (giới hạn bằng option limits: { fileSize: 5 1024 1024 } cho 5MB) và loại file qua mimetype để chặn file độc hại.
Trong môi trường production, không nên lưu file vào server local mà upload thẳng lên cloud storage như AWS S3 hoặc Google Cloud Storage để đảm bảo scalability và reliability. Với file lớn hàng trăm MB trở lên, nên dùng stream upload thay vì đọc toàn bộ file vào memory để tránh crash server.
Streams xử lý data theo từng phần nhỏ (chunks) thay vì load toàn bộ vào memory — rất quan trọng khi xử lý file lớn (CSV hàng GB, video).
Có 4 loại: Readable (đọc data, ví dụ fs.createReadStream), Writable (ghi data, ví dụ fs.createWriteStream), Duplex (vừa đọc vừa ghi, ví dụ TCP socket), Transform (biến đổi data trong quá trình truyền, ví dụ compression). Dùng .pipe() để nối streams: fs.createReadStream('big.csv').pipe(transform).pipe(res).
Nếu đọc file 2GB bằng fs.readFile sẽ cần 2GB RAM, nhưng Stream chỉ cần vài MB.
process.env là object chứa tất cả environment variables của hệ điều hành mà Node.js process có thể truy cập, ví dụ process.env.DATABASE_URL để lấy connection string. Thư viện dotenv đọc file .env ở root project và inject các biến vào process.env lúc khởi động app, giúp developer không cần set biến môi trường thủ công.
Nguyên tắc quan trọng: file .env phải nằm trong .gitignore để không commit secrets lên repository, và tạo file .env.example chứa danh sách biến cần thiết (không có giá trị thật) làm template cho team.
Nên validate tất cả env vars lúc app startup bằng thư viện như Zod hoặc envalid để crash sớm nếu thiếu biến, thay vì gặp lỗi runtime khó debug khi app đang chạy.
Zod là lựa chọn tốt nhất cho Express + TS: schema-first, type-safe, safeParse trả structured errors.
const schema = z.object({ email: z.string().email(), age: z.number().min(18) });
app.post('/users', (req, res) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);
});Alternatives: Joi, Yup, class-validator.
Validate ở layer đầu tiên — trước bất kỳ business logic nào.
Logging trong production cần có cấu trúc và chiến lược rõ ràng để debug hiệu quả khi có sự cố. Nên dùng Pino (nhanh nhất, gấp 5 lần Winston) hoặc Winston (linh hoạt hơn, nhiều transports), và format log dưới dạng JSON thay vì text thuần vì JSON dễ parse bởi log aggregators như ELK Stack, Datadog, hay CloudWatch.
Phân chia log levels hợp lý: error cho lỗi cần xử lý ngay, warn cho tình huống bất thường nhưng chưa lỗi, info cho business events quan trọng, debug cho chi tiết kỹ thuật chỉ bật khi cần.
Mỗi request nên có correlation ID (UUID) truyền qua tất cả services để trace toàn bộ luồng xử lý, và tuyệt đối không log sensitive data như passwords, tokens, hay thông tin cá nhân người dùng.
Socket.io wraps WebSocket với automatic reconnection, rooms, và namespace support.
// Server
const io = new Server(httpServer);
io.on('connection', socket => {
socket.on('message', data => { io.emit('message', data); });
});
// Client
const socket = io('http://localhost:3000');
socket.emit('message', 'hello');Rooms cho group chat, namespaces cho tách concerns.
Reconnection tự động.
Node.js chạy JavaScript trên một thread duy nhất, nhưng vẫn xử lý được nhiều request đồng thời nhờ cơ chế non-blocking I/O và event loop. Khi có tác vụ I/O (đọc file, gọi database, HTTP request), Node.js giao cho libuv xử lý trong thread pool riêng và tiếp tục nhận request mới. Khi tác vụ I/O hoàn thành, callback được đưa vào event queue và event loop sẽ đẩy lên call stack khi stack trống.
Tuy nhiên, nếu có tác vụ tính toán nặng (CPU-intensive) thì sẽ block main thread, lúc này cần dùng Worker Threads để chạy song song.
Callback là cách xử lý bất đồng bộ đầu tiên trong JavaScript, nhưng dễ dẫn đến callback hell khi lồng nhiều tầng và khó xử lý lỗi vì phải kiểm tra error ở mỗi callback.
- Promise cải thiện bằng cách dùng .then() để chain và .catch() để bắt lỗi tập trung, nhưng vẫn có thể dài dòng.
- Async/await là cú pháp mới nhất, dùng try/catch để bắt lỗi giống code đồng bộ, dễ đọc và debug nhất.
- Khuyến nghị: dùng async/await làm mặc định, Promise.all() khi cần chạy song song nhiều request, và Promise.allSettled() khi muốn biết kết quả của tất cả request dù có lỗi.
Environment variable dùng để lưu trữ cấu hình thay đổi theo từng môi trường (development, staging, production) như database URL, API key, port number, mà không cần thay đổi code. File .env chứa các biến này cho môi trường phát triển local, và package dotenv sẽ tự động load chúng vào process.env.
Tuyệt đối không được commit file .env lên git vì chứa thông tin nhạy cảm — thay vào đó tạo file .env.example chỉ chứa tên biến không có giá trị để người khác biết cần cấu hình gì. Trên production, environment variable được cấu hình trực tiếp trên hosting platform như Vercel, AWS, hoặc Docker.
process.nextTick() chạy trước cả Promise microtasks (không thuộc event loop phase) — recursive nextTick sẽ starve I/O; dùng setImmediate() trong hầu hết cases. process.nextTick() không thuộc bất kỳ phase nào của event loop — callbacks được đặt vào nextTick queue và chạy ngay sau synchronous code hiện tại kết thúc, trước khi event loop tiếp tục phase tiếp theo (kể cả trước Promise microtasks). setImmediate() thuộc check phase.
- Thứ tự ưu tiên: synchronous code → nextTick queue → Promise microtasks → event loop phases (timers → poll → check/setImmediate).
- Starvation danger: recursive
process.nextTick()sẽ block event loop mãi mãi — không bao giờ cho I/O callbacks chạy. - Node.js docs khuyên dùng
setImmediate()thay nextTick trong hầu hết trường hợp. - Khi nextTick phù hợp: emit event sau constructor return (để listener đăng ký kịp), propagate error asynchronously trong API mà phải consistent async.
- Khi setImmediate phù hợp: chia nhỏ heavy computation qua nhiều iterations mà vẫn cho I/O xen vào.
Stream xử lý data theo chunks — memory efficiency cực cao: đọc file 1GB chỉ tốn ~64KB RAM (default highWaterMark) thay vì 1GB.
- Bốn loại: Readable (fs.createReadStream, HTTP req, process.stdin), Writable (fs.createWriteStream, HTTP res, process.stdout), Duplex (TCP socket — đọc và ghi độc lập), Transform (zlib.createGzip, crypto.createCipher — transform data flow through).
pipeline()API (Node.js 10+) thay thế.pipe():await pipeline(readStream, transformStream, writeStream)— tự cleanup và propagate errors (.pipe()không handle errors tốt). - Stream consumers với
for await...of:for await (const chunk of readableStream) { process(chunk); }— ergonomic nhất.
Ví dụ thực: serve 1GB file với 0 timeout và thấp RAM: const src = fs.createReadStream(filePath); const gzip = zlib.createGzip(); await pipeline(src, gzip, res) — data chảy qua, không buffer toàn bộ.
- Backpressure: khi Writable chậm hơn Readable,
pipelinetự pause Readable tránh OOM — đây là lý do không dùng.pipe()thủ công trong production.
Libuv là C library đa nền tảng cung cấp event loop, async I/O và thread pool cho Node.js.
Hai loại operations:
- OS-native async (không dùng thread pool): network I/O (TCP/UDP) — dùng epoll (Linux), kqueue (macOS), IOCP (Windows), cực efficient;
- Thread pool operations (dùng 4 threads mặc định vì OS không hỗ trợ async đầy đủ): file system I/O (fs.readFile), DNS lookup (dns.lookup — không phải dns.resolve), crypto operations (pbkdf2, scrypt, randomBytes)
Thread pool size: tăng bằng env var UV_THREADPOOL_SIZE=16 (max 1024) — quan trọng khi app làm nhiều file I/O hoặc crypto đồng thời, 4 threads mặc định tạo bottleneck.
Ví dụ vấn đề: app gọi 10 dns.lookup() đồng thời, 4 threads xử lý 4 cái, 6 cái còn lại queue — tăng UV_THREADPOOL_SIZE hoặc dùng dns.resolve() (network I/O, không dùng thread pool).
Libuv abstract sự khác biệt giữa OS — cùng Node.js code chạy trên Linux epoll và Windows IOCP mà không cần sửa.
Express 5 tự động forward async errors đến error handler — không cần try/catch hay asyncHandler wrapper; path matching cải tiến và một số breaking routing syntax changes so với v4. Express 5 (stable 2024): async error handling tự động (throw trong async route tự chuyển đến error handler, không cần try/catch), path matching cải tiến, một số breaking changes về routing syntax.
Breaking changes từ v4: app.router bị xóa (mount router trực tiếp), regex path không dùng thẳng mà qua path-to-regexp, req.param() bị xóa (dùng req.params.name). Express 4 yêu cầu wrap async routes với try/catch hoặc dùng express-async-handler. Thực tế migration: thay toàn bộ asyncHandler(fn) wrapper bằng async route thẳng, kiểm tra path patterns với regex, update req.param() calls.
Access token short-lived (15 phút) gửi trong mỗi request — stateless, server không lưu gì.
- Refresh token long-lived (7-30 ngày) lưu trong httpOnly cookie, chỉ dùng để lấy access token mới tại endpoint
/auth/refresh. - Rotation strategy: mỗi lần dùng refresh token phải issue cặp token mới và invalidate token cũ (one-time-use) — nếu token cũ bị dùng lại là dấu hiệu bị đánh cắp, revoke toàn bộ session.
- Token blacklisting: lưu revoked JTI (JWT ID) trong Redis với TTL bằng thời gian còn lại của token — kiểm tra blacklist trong auth middleware.
- Sliding sessions: mỗi request hợp lệ extend session — issue refresh token mới nếu còn < 1 ngày.
- Security: lưu hash của refresh token trong DB, không plain text — nếu DB leak thì token vô dụng.
Pitfall: không implement rotation → stolen refresh token dùng mãi đến hết hạn; không lưu vào httpOnly cookie → XSS đánh cắp được.
Node.js single-threaded chỉ dùng một CPU core — server 16-core lãng phí 15 cores.
- Cluster module: master process fork N worker processes (thường = số CPU cores:
os.cpus().length), mỗi worker là Node.js process độc lập, share cùng port. - Connection distribution: Linux dùng round-robin (master nhận connection rồi distribute), Windows dùng OS scheduling.
- IPC communication:
process.send(msg)từ worker,worker.on('message', handler)ở master — dùng để share state nhỏ như worker health. - Shared nothing architecture: workers không share memory, mỗi worker có heap riêng — session/cache phải dùng Redis thay vì in-memory.
- Sticky sessions: cần khi dùng WebSocket — load balancer phải route cùng client đến cùng worker (nginx
ip_hash). - PM2 cluster mode:
pm2 start app.js -i maxtự động quản lý cluster với zero-downtime reload (pm2 reload app) — reload từng worker một thay vì restart toàn bộ. - Khi nào dùng cluster vs reverse proxy: cluster cho vertical scaling trên một server, Nginx/HAProxy cho horizontal scaling nhiều servers.
Worker Threads chạy JS trong separate threads trong cùng process — chia sẻ memory qua SharedArrayBuffer, dùng Atomics để synchronize.
- Cluster tạo separate OS processes — không share memory, overhead IPC cao hơn.
- Worker Threads API:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'). - Main thread:
new Worker('./worker.js', { workerData: { input } }), lắng ngheworker.on('message', result => ...). - Worker thread:
parentPort.postMessage(result). - Message passing qua
postMessage()— data được structured clone (deep copy) trừ khi transfer ownership với Transferable (ArrayBuffer). - SharedArrayBuffer + Atomics: chia sẻ raw memory không cần copy — dùng cho real-time collaboration, game state.
- Use cases thực tế: image resizing (sharp library), PDF generation, crypto operations, ML inference, large JSON parsing.
Pitfall: Worker Threads không tăng tốc I/O-bound tasks — Node.js event loop đã xử lý I/O async; chỉ dùng cho CPU-bound tasks chiếm event loop > 10ms.
- Communication patterns: sync REST/gRPC (request-response, coupling chặt hơn, dễ debug) vs async message queue — RabbitMQ/Kafka (loose coupling, resilient, nhưng eventual consistency).
- Service discovery: mỗi service đăng ký địa chỉ vào registry (Consul, etcd), services khác lookup thay vì hardcode URL — cần thiết khi services scale up/down động.
- API Gateway: single entry point cho clients, xử lý auth/rate-limiting/routing, tránh clients phải biết topology nội bộ (Kong, AWS API Gateway).
- Saga pattern cho distributed transactions: Choreography (services emit events, services khác react — loose coupling) vs Orchestration (central saga orchestrator điều phối — dễ debug hơn).
- Event sourcing: lưu toàn bộ events thay vì current state — audit log hoàn chỉnh, có thể replay để rebuild state.
- Nhược điểm thực tế: distributed tracing cần setup (Jaeger, Zipkin), network latency thay vì function call, integration tests phức tạp hơn, debugging khó hơn.
Pitfall: microservices quá sớm — monolith trước, tách khi có bounded contexts rõ ràng và team đủ lớn.
Event loop blocking: synchronous code chiếm CPU lâu khiến event loop không xử lý được I/O callbacks, timers, requests khác.
Ví dụ thực tế:
JSON.parse(fs.readFileSync('huge.json'))— 200MB JSON parse tốn ~2s block hoàn toàn;- regex backtracking —
/^(a+)+$/.test('aaaaaaaaab')chạy exponential time với input độc hại (ReDoS); - crypto sync —
crypto.pbkdf2Sync()trong request handler; - nested loops O(n²) trên large arrays
Đo blocking: node --prof app.js tạo V8 profile; clinic doctor -- node app.js từ clinic.js package hiển thị event loop lag rõ ràng; perf_hooks API đo eventLoopUtilization().
Fix: chia nhỏ task với setImmediate() để yield event loop giữa chunks; offload sang Worker Threads cho CPU-intensive; dùng streaming thay vì load toàn bộ vào memory; safe-regex package detect ReDoS vulnerable patterns.
Threshold: bất kỳ synchronous operation > 10ms trong request handler là vấn đề cần xem xét.
- Backpressure: Readable produce data nhanh hơn Writable consume — nếu không xử lý, data buffer trong memory vô hạn cho đến khi OOM crash. highWaterMark: ngưỡng buffer size (default 16KB cho object streams, 16384 bytes cho byte streams) — khi buffer vượt ngưỡng, stream signals slow down.
- Cơ chế manual:
writable.write(chunk)trả vềfalsekhi internal buffer đầy → stop reading; lắng nghewritable.on('drain', resume)để tiếp tục.
Ví dụ file upload: client upload 1GB/s, DB write chỉ 100MB/s — không có backpressure thì 900MB/s tích lũy trong RAM. pipe() tự xử lý nhưng không handle errors tốt. pipeline() API (stream/promises): await pipeline(req, transform, fs.createWriteStream(dest)) — tự xử lý backpressure VÀ cleanup tất cả streams khi error/complete, không leak.
- Transform streams: implement
_transform(chunk, enc, callback), gọicallback()khi sẵn sàng nhận chunk tiếp theo — tự nhiên tạo backpressure.
Pitfall: .on('data', handler) puts stream in flowing mode bỏ qua backpressure — dùng async iteration for await (const chunk of readable) thay thế.
EventEmitter implement Observer/pub-sub pattern — backbone của Node.js internals (streams, http.Server, process đều extend EventEmitter).
- API cơ bản:
.on('event', listener)đăng ký persistent listener,.once('event', listener)tự remove sau lần emit đầu tiên,.emit('event', ...args)kích hoạt đồng bộ tất cả listeners,.off('event', listener)hoặc.removeListener()để unsubscribe. - Memory leak warning: Node.js warn khi > 10 listeners trên cùng event (
MaxListenersExceededWarning) — dùngemitter.setMaxListeners(20)nếu cố ý, hoặc fix leak. - Thường quên
.removeListener()trong component unmount/cleanup → listeners tích lũy. - Error convention: event tên
'error'đặc biệt — nếu emit'error'mà không có listener thì throw uncaughtException crash process. - Practical patterns:
class OrderService extends EventEmitter { async createOrder(data) { const order = await db.save(data); this.emit('order:created', order); } }— services khác lắng nghe event thay vì coupling trực tiếp..once()dùng cho one-time setup:server.once('listening', () => console.log('ready')). - Async listeners: EventEmitter không await async listeners — unhandled promise trong listener → silent failure.
Graceful shutdown đảm bảo zero data loss và zero error responses khi deploy/restart.
- Implementation pattern:
process.on('SIGTERM', async () => { server.close(async () => { await db.disconnect(); await redis.quit(); process.exit(0); }); })—server.close()stop accepting new connections nhưng chờ existing connections finish. - Connection draining: set timeout 30s cho in-flight requests — nếu quá timeout force close.
- Health check: Kubernetes gửi SIGTERM sau khi remove pod khỏi load balancer (readiness probe fails) → khoảng trống 5-10s để LB drain traffic trước khi process nhận SIGTERM — config
terminationGracePeriodSeconds: 30trong K8s. - Kubernetes flow: SIGTERM → preStop hook (sleep 5s để LB drain) → app nhận SIGTERM → graceful shutdown → SIGKILL nếu quá
terminationGracePeriodSeconds. - Track in-flight:
let connections = 0; app.use((req, res, next) => { connections++; res.on('finish', () => connections--); next(); })— chờ connections về 0 hoặc timeout.
Pitfall: không handle SIGINT (Ctrl+C local dev) — thêm cả process.on('SIGINT', handler).
- Docker
STOPSIGNAL SIGTERM+ENTRYPOINTdùng exec form (không shell) để process nhận signal trực tiếp.
PM2 là production process manager không thể thiếu cho Node.js — cluster mode tận dụng multi-core CPU, pm2 reload cho zero-downtime deploy, max_memory_restart auto-restart khi memory leak.
- PM2 là production process manager cho Node.js — giải quyết auto-restart, clustering, logging, monitoring trong một tool.
- Ecosystem config (
ecosystem.config.js):module.exports = { apps: [{ name: 'api', script: 'dist/index.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, max_memory_restart: '500M', error_file: 'logs/err.log', out_file: 'logs/out.log' }] }—pm2 start ecosystem.config.js. - Zero-downtime reload:
pm2 reload apirestart từng worker một (rolling restart), không nhưpm2 restartkill tất cả cùng lúc — dùng reload cho deploys. - Monitoring dashboard:
pm2 monitreal-time CPU/RAM per process;pm2 statusoverview tất cả processes. - Log management:
pm2 logs api --lines 100xem logs;pm2 flushclear logs; tích hợp với logrotate để tránh disk full. - Startup scripts:
pm2 startupgenerate systemd/init script để PM2 tự start khi server reboot;pm2 savelưu current process list.max_memory_restart: tự restart process khi vượt ngưỡng RAM — safety net cho memory leaks.
Pitfall: PM2 cluster mode dùng shared nothing — sessions/cache phải ở Redis; pm2 reload không graceful nếu app không handle SIGINT/SIGTERM.
Caching cho Node.js API hoạt động theo nhiều tầng, mỗi tầng phục vụ mục đích khác nhau. Tầng in-memory dùng node-cache hoặc Map để cache dữ liệu truy cập thường xuyên ngay trong process, truy xuất cực nhanh nhưng mất khi restart. Tầng distributed dùng Redis để cache chia sẻ giữa nhiều server instances, phù hợp cho production.
Pattern phổ biến nhất là cache-aside: kiểm tra cache trước, nếu miss thì query database rồi set vào cache với TTL, ví dụ cache key dạng users:${id}:profile với TTL 5 phút.
Phần khó nhất là invalidation — có thể dùng TTL-based (tự hết hạn sau thời gian) hoặc event-based (khi data thay đổi thì xóa cache liên quan), và cần cẩn thận với cache stampede khi nhiều requests cùng miss cache đồng thời.
Trong production Node.js, cần phân biệt rõ hai loại lỗi để xử lý khác nhau. Operational errors là lỗi dự kiến được như user gửi data sai (400), không có quyền (401), hoặc resource không tồn tại (404) — xử lý gracefully bằng cách trả HTTP status phù hợp và message rõ ràng.
Programming errors là bugs thật sự như TypeError hay null reference — cách tốt nhất là để process crash rồi tự restart bằng PM2 hoặc Docker, vì app đang ở trạng thái không xác định. Cần có global handlers process.on('uncaughtException') và process.on('unhandledRejection') để log lỗi trước khi crash.
Kết hợp logging library như Winston hoặc Pino với log levels (error, warn, info, debug) và format JSON để dễ parse bởi log aggregators.
Tạo pool connections sẵn, tái sử dụng thay vì mở/đóng mỗi query.
new Pool({ max: 20, idleTimeoutMillis: 30000 })Không có pooling: mỗi request mở connection mới → chậm, exhaust DB connections.
- Prisma, Sequelize tự quản lý pool.
- Monitor: active/idle/waiting connections.
Microservices giao tiếp với nhau qua hai cách chính: synchronous và asynchronous. Synchronous dùng REST (đơn giản, phổ biến) hoặc gRPC (nhanh hơn nhờ Protocol Buffers và HTTP/2, type-safe, phù hợp internal services). Asynchronous dùng message queue như RabbitMQ cho task queue đơn giản, Redis pub/sub cho real-time events, hoặc Kafka cho high-throughput event streaming.
Pattern API Gateway đặt ở trước làm single entry point, xử lý routing, authentication, và rate limiting tập trung thay vì mỗi service tự làm.
Circuit breaker pattern rất quan trọng: khi một service lỗi, tạm ngắt kết nối đến service đó thay vì tiếp tục gửi requests gây cascade failure — thư viện opossum cho Node.js hỗ trợ pattern này.