Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Danh mục

Node.js iconNode.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, crypto và 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.

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.

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 react cài vào dependencies — những gì cần thiết để app chạy trên production. npm install --save-dev jest typescript eslint cài vào devDependencies — 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).

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: import phả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, dynamic import().
  • Interop challenges: CJS có thể require() ESM file? Không được, phải dùng dynamic import().
  • 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ả .js là 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 http module 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 http module 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ọi next() và không res.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.example tạ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.ts module export typed config thay vì access process.env trự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.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')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__filename không tồn tại trong file .mjs hoặc khi type: module trong package.json.

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:

  1. Timers: thực thi callbacks của setTimeout/setInterval đã đến hạn;
  2. Pending callbacks: I/O errors từ vòng trước;
  3. Idle/prepare: internal use;
  4. Poll: fetch I/O events mới — nếu queue empty và không có timers pending, block tại đây chờ I/O;
  5. Check: setImmediate callbacks;
  6. 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'.

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 err mỗ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; parallel const [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.

Pitfall #2 — error không được handle: async function foo() { await riskyOp(); } gọi foo() mà không await/catch = unhandled rejection.

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ới crypto.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.ts export router, routes/index.ts aggregate tất cả routers, app.ts chỉ 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.

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:

  1. kiểm tra response header Access-Control-Allow-Origin trong DevTools Network tab,
  2. credentials: true nhưng server trả * → browser chặn,
  3. đặ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.

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-limit dùng fixed window: rateLimit({ windowMs: 15601000, max: 100, standardHeaders: true, legacyHeaders: false }).
  • Distributed rate limiting với Redis: rate-limit-redis store để 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ó exp ngắ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 argon2 package 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:

  1. try/catch trong async functions là chính,
  2. .catch() chain cho fire-and-forget promises,
  3. 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:

  1. 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;
  2. CLI tools;
  3. 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.jsnode --prof-process isolate-*.log → readFileSync trong hot path hiện rõ ràng với % CPU time.

  • Breakpoint debugging: node --inspect app.js expose debugger tại port 9229; mở chrome://inspect → attach. --inspect-brk dừ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: true trong 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ền 1 OR 1=1 dump 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-regex package kiểm tra pattern, timeout regex execution.
  • Directory traversal: fs.readFile('/uploads/' + req.query.file) với file=../../etc/passwd.
  • Fix: path.resolve() + verify kết quả starts with allowed directory.
  • Prototype pollution: obj[userKey] = userValue với key __proto__ corrupt Object prototype.
  • Fix: Object.create(null) cho plain objects, validate keys.
  • Dependency vulnerabilities: npm audit + npm audit fix, snyk cho 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, next objects — verify next() được gọi hay res.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: globalSetup chạy migration một lần, beforeEach truncate data, afterAll disconnect.
  • 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.

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

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