jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Node.js Super Senior · Phase 1 — Core Fundamentals

Phase 1: how Node really works (V8, libuv, bindings), the event loop phase-by-phase, the libuv thread pool, async from callbacks to async/await, ESM vs CommonJS, the core modules that matter, worker threads vs cluster, and memory.

Welcome to Phase 1 of a 10-phase series (plus bonus deep dives) designed to take you from gà mờ to Super Senior Node.js backend developer {Chào mừng tới Phase 1 của series 10 phase (cộng các deep dive bonus) đưa bạn từ gà mờ thành Super Senior Node.js backend developer}. This isn’t a tour of APIs — it’s a path to production-ready, enterprise-grade engineering {Đây không phải chuyến dạo API — mà là con đường tới kỹ thuật production-ready, đẳng cấp enterprise}. Each phase is hands-on and ends with real projects {Mỗi phase đều thực hành và kết thúc bằng dự án thật}.

We use Node.js 24 LTS and TypeScript throughout {Ta dùng Node.js 24 LTSTypeScript xuyên suốt}. Phase 1 is the longest and most important: everything later — Express, databases, auth, performance — is just patterns on top of these primitives {Phase 1 dài và quan trọng nhất: mọi thứ sau này — Express, database, auth, hiệu năng — chỉ là các mẫu xây trên những primitive này}.


The roadmap {Lộ trình}

PHASE 1  → PHASE 2 → PHASE 3 → PHASE 4 → PHASE 5
Core       HTTP      Express   Database  Auth



PHASE 6  → PHASE 7 → PHASE 8 → PHASE 9 → PHASE 10
Patterns   DevOps    Perf      Testing   Architecture

      ↓  (bonus deep dives)

PHASE 11 → PHASE 12 → PHASE 13
PostgreSQL  Prisma     NestJS
  1. Core Fundamentals — event loop, async, core modules (you are here) {event loop, async, core module (bạn đang ở đây)}.
  2. HTTP & Web — the protocol, a raw server, the middleware concept {giao thức, server thô, khái niệm middleware}.
  3. Express Mastery — routing, middleware, error handling {routing, middleware, xử lý lỗi}.
  4. Database Integration — SQL & NoSQL, ORMs, transactions {SQL & NoSQL, ORM, transaction}.
  5. Auth & Security — Passport, JWT, OAuth2, RBAC {Passport, JWT, OAuth2, RBAC}.
  6. Advanced Patterns — DI, repository, caching, queues {DI, repository, caching, queue}.
  7. DevOps & Deployment — Docker, PM2, CI/CD {Docker, PM2, CI/CD}.
  8. Performance — profiling, optimization, clustering {profiling, tối ưu, clustering}.
  9. Testing — unit, integration, E2E, coverage {unit, integration, E2E, coverage}.
  10. Enterprise Architecture — clean architecture, microservices, CQRS, SOLID {clean architecture, microservices, CQRS, SOLID}.

Now, Phase 1 {Giờ là Phase 1}.


1. What Node.js actually is {Node.js thực sự là gì}

Most beginners think “Node = JavaScript on the server.” True, but it hides the design that makes Node special {Đa số người mới nghĩ “Node = JavaScript trên server.” Đúng, nhưng nó che giấu thiết kế làm nên sự đặc biệt của Node}. Node.js is a C++ program that embeds a JavaScript engine and an async I/O library {Node.js là một chương trình C++ nhúng một engine JavaScript và một thư viện I/O bất đồng bộ}. Three layers do the work {Ba tầng làm việc}:

  • V8 — Google’s engine. It parses, compiles (JIT) and runs your JS, and manages the heap + garbage collector {engine của Google. Nó parse, compile (JIT) và chạy JS, đồng thời quản lý heap + bộ thu gom rác}.
  • libuv — a C library that gives Node its event loop, a thread pool, and a cross-platform async I/O abstraction (epoll on Linux, kqueue on macOS/BSD, IOCP on Windows) {thư viện C cho Node event loop, một thread pool, và lớp trừu tượng I/O bất đồng bộ đa nền tảng}.
  • Node bindings + core JS — C++ “bindings” expose OS features to JS, and a layer of JavaScript (fs, http, events…) wraps them in the friendly API you call {“binding” C++ phơi tính năng OS cho JS, và một tầng JavaScript bọc chúng thành API thân thiện bạn gọi}.
        Your JavaScript / TypeScript
   ┌─────────────────────────────────────────────┐
   │  Node core JS:  fs  http  events  stream     │  ← what you import
   ├─────────────────────────────────────────────┤
   │  C++ bindings (process.binding / internalBinding)
   ├──────────────────────────┬──────────────────┤
   │  V8                       │  libuv           │
   │  • parse + JIT compile JS │  • event loop    │
   │  • heap + garbage collect │  • thread pool   │
   │                           │  • async I/O     │
   ├──────────────────────────┴──────────────────┤
   │                Operating System              │
   └─────────────────────────────────────────────┘

The single-threaded, event-driven model is the core idea {Mô hình đơn luồng, hướng sự kiện là ý tưởng cốt lõi}: instead of one OS thread per request (each one waiting on I/O), Node runs one main thread that never waits — it offloads slow I/O to libuv, registers a callback, and immediately moves to the next task {thay vì một luồng OS mỗi request (mỗi luồng chờ I/O), Node chạy một luồng chính không bao giờ chờ — đẩy I/O chậm sang libuv, đăng ký callback, rồi lập tức sang việc kế}.

“Single-threaded” is about your JavaScript {“Đơn luồng” là nói về JavaScript của bạn}. Under the hood libuv keeps a small pool of OS threads, and the kernel does I/O in parallel — so Node handles tens of thousands of concurrent connections on one core {Bên dưới libuv vẫn giữ một pool nhỏ các luồng OS, và kernel làm I/O song song — nên Node xử lý hàng chục nghìn kết nối đồng thời trên một core}.


2. The event loop, phase by phase {Event loop, từng pha một}

This is the single most asked senior interview topic, and the source of every “why did this log out of order?” bug {Đây là chủ đề phỏng vấn senior được hỏi nhiều nhất, và là nguồn của mọi bug “sao nó in sai thứ tự?”}.

After Node runs your top-level synchronous code, libuv enters a loop with distinct phases, each with its own callback queue {Sau khi Node chạy code đồng bộ top-level, libuv vào một vòng lặp với các pha riêng biệt, mỗi pha có hàng đợi callback riêng}:

   ┌───────────────────────────┐
┌─►│           timers          │  setTimeout / setInterval callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │     pending callbacks     │  some system/TCP error callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │       idle, prepare       │  internal only
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │           poll            │  ← retrieve I/O events; may BLOCK here
│  └─────────────┬─────────────┘     (this is where Node "waits")
│  ┌─────────────▼─────────────┐
│  │           check           │  setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────▼─────────────┐
│  │      close callbacks      │  socket.on('close'), etc.
│  └─────────────┬─────────────┘
└────────────────┘  loop again while there is work

The phase you care about most is poll: this is where the loop parks and waits for I/O to complete (a file read, an incoming connection, a DB reply) {Pha bạn cần quan tâm nhất là poll: đây là nơi loop đỗ lại và chờ I/O hoàn tất}. If there’s nothing else to do, Node sleeps here efficiently instead of busy-spinning {Nếu không còn việc, Node ngủ ở đây hiệu quả thay vì quay vòng tốn CPU}.

Microtasks: the queues between phases {Microtask: hàng đợi giữa các pha}

Promises do not live in those libuv phases. They run in two microtask queues that Node drains after every single callback and between phases {Promise không nằm trong các pha libuv kia. Chúng chạy trong hai hàng microtask mà Node rút cạn sau mỗi callback và giữa các pha}:

  1. The process.nextTick queue — highest priority {hàng process.nextTick — ưu tiên cao nhất}.
  2. The Promise microtask queue (.then, await, queueMicrotask) — drained right after nextTick {hàng microtask Promise — rút ngay sau nextTick}.

The golden rule that explains every weird ordering {Quy tắc vàng giải thích mọi thứ tự kỳ lạ}: whenever the call stack empties, Node fully drains nextTick, then fully drains Promise microtasks, before taking the next macrotask {mỗi khi call stack rỗng, Node rút cạn nextTick, rồi rút cạn microtask Promise, trước khi lấy macrotask kế}.

console.log('1: sync start');

setTimeout(() => console.log('2: setTimeout (timers phase)'), 0);
setImmediate(() => console.log('3: setImmediate (check phase)'));

Promise.resolve().then(() => console.log('4: promise microtask'));
process.nextTick(() => console.log('5: nextTick — beats promises'));

console.log('6: sync end');

// Output:
// 1: sync start
// 6: sync end
// 5: nextTick — beats promises
// 4: promise microtask
// 2: setTimeout      (order of 2 vs 3 here is not guaranteed)
// 3: setImmediate

setTimeout(fn, 0) vs setImmediate(fn) ordering is not guaranteed at the top level (depends on loop timing) {thứ tự setTimeout(fn, 0) vs setImmediate(fn) ở top-level không đảm bảo (tùy thời điểm loop)}. But inside an I/O callback, setImmediate always fires before setTimeout, because the loop is already past timers and hits the check phase next {Nhưng bên trong một I/O callback, setImmediate luôn chạy trước setTimeout, vì loop đã qua timers và tới check kế tiếp}.

process.nextTick is a footgun {process.nextTick là cái bẫy}

Because nextTick drains completely before the loop continues, a recursive nextTick starves the event loop — I/O never gets a turn {Vì nextTick rút cạn hoàn toàn trước khi loop tiếp tục, một nextTick đệ quy bỏ đói event loop — I/O không bao giờ tới lượt}:

// ❌ This freezes all I/O forever — the poll phase is never reached
function loop() { process.nextTick(loop); }
loop();

Senior rule {Quy tắc senior}: prefer queueMicrotask or setImmediate for “run soon”; reserve process.nextTick for the narrow case of letting a caller attach listeners before you emit {ưu tiên queueMicrotask hoặc setImmediate cho “chạy sớm”; chỉ dùng process.nextTick cho trường hợp hẹp: cho caller gắn listener trước khi bạn emit}.

The libuv thread pool — the part people forget {Thread pool của libuv — phần người ta hay quên}

Network I/O (TCP/HTTP sockets) is truly async at the OS level, so it uses epoll/kqueue and no threads {I/O mạng (socket TCP/HTTP) bất đồng bộ thật ở mức OS, nên dùng epoll/kqueuekhông cần thread}. But some operations have no async OS API, so libuv runs them on a thread pool (default size 4) {Nhưng vài thao tác không có API async ở OS, nên libuv chạy chúng trên thread pool (mặc định 4 luồng)}:

  • fs.* filesystem operations {thao tác hệ thống tệp}
  • dns.lookup (but not dns.resolve) {dns.lookup (nhưng không phải dns.resolve)}
  • crypto (pbkdf2, scrypt, randomBytes…) and zlib compression {crypto và nén zlib}
# If your app does heavy crypto/fs, the default 4 threads bottleneck.
# Raise it BEFORE the process starts (read once at startup):
UV_THREADPOOL_SIZE=16 node server.js

The senior insight: a slow bcrypt.hash doesn’t block the event loop, but it does occupy a thread-pool slot — fire five of them with a pool of four and the fifth queues {Hiểu biết senior: một bcrypt.hash chậm không chặn event loop, nhưng nó chiếm một slot thread-pool — chạy năm cái với pool bốn thì cái thứ năm phải xếp hàng}.


3. Asynchronous programming, the senior way {Lập trình bất đồng bộ, kiểu senior}

The evolution: callbacks → Promises → async/await {Tiến hóa}

Node started with error-first callbacks {Node khởi đầu với callback error-first}. Nest a few and you get callback hell — code drifting rightward, error handling duplicated everywhere {Lồng vài cái và bạn có callback hell — code trôi sang phải, xử lý lỗi lặp khắp nơi}:

import { readFile } from 'node:fs';

readFile('a.txt', 'utf8', (err, a) => {
  if (err) return console.error(err);
  readFile('b.txt', 'utf8', (err2, b) => {  // rightward drift begins...
    if (err2) return console.error(err2);
    // ...the abyss
  });
});

Promises are state machines with three states — pending → fulfilled or pending → rejected — that can only settle once {Promise là máy trạng thái ba trạng thái — pending → fulfilled hoặc pending → rejected — và chỉ settle một lần}. async/await is syntax sugar over them that reads like sync code while staying non-blocking {async/await là cú pháp đường trên chúng, đọc như code đồng bộ mà vẫn non-blocking}:

import { readFile } from 'node:fs/promises'; // the promise-based variant

async function load(): Promise<[string, string]> {
  const a = await readFile('a.txt', 'utf8');
  const b = await readFile('b.txt', 'utf8');
  return [a, b];
}

Sequential vs parallel — the #1 performance reflex {Tuần tự vs song song — phản xạ hiệu năng số 1}

The load above reads two independent files one after another — wasting time {load ở trên đọc hai file độc lập lần lượt — lãng phí thời gian}:

// ❌ Sequential — ~200ms if each takes 100ms
const a = await readFile('a.txt', 'utf8');
const b = await readFile('b.txt', 'utf8');

// ✅ Parallel — ~100ms; fire both, then await together
const [a, b] = await Promise.all([
  readFile('a.txt', 'utf8'),
  readFile('b.txt', 'utf8'),
]);

Rule {Quy tắc}: await in sequence only when a step truly depends on the previous; otherwise Promise.all {await tuần tự chỉ khi một bước thật sự phụ thuộc bước trước; nếu không thì Promise.all}.

The four Promise combinators — know exactly when to use each {Bốn combinator Promise — biết chính xác khi nào dùng}

// all      → resolves with ALL results; rejects on the FIRST failure (fail-fast)
const users = await Promise.all(ids.map((id) => fetchUser(id)));

// allSettled → never rejects; gives {status, value|reason} per item (resilient)
const results = await Promise.allSettled(ids.map((id) => fetchUser(id)));
const ok = results.filter((r) => r.status === 'fulfilled');

// race     → settles with the FIRST to settle (win or fail) — used for timeouts
// any      → resolves with the FIRST to SUCCEED; rejects only if ALL fail
const fastest = await Promise.any([fromCacheA(), fromCacheB()]);

Concurrency control — don’t DoS your own dependencies {Kiểm soát đồng thời — đừng tự DoS dependency của mình}

Promise.all over 10,000 items opens 10,000 connections at once and melts your DB {Promise.all trên 10.000 item mở 10.000 kết nối cùng lúc và làm chảy DB}. Seniors cap concurrency {Senior giới hạn độ đồng thời}:

/** Run an async mapper over items with a hard concurrency limit. */
async function mapLimit<T, R>(
  items: readonly T[],
  limit: number,
  fn: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let cursor = 0;

  async function worker(): Promise<void> {
    while (cursor < items.length) {
      const index = cursor++;            // claim a slot atomically (single-threaded)
      results[index] = await fn(items[index], index);
    }
  }

  // Spin up `limit` workers that pull from the shared cursor.
  const size = Math.min(limit, items.length);
  await Promise.all(Array.from({ length: size }, worker));
  return results;
}

// 1000 URLs, at most 8 in flight at any moment
const bodies = await mapLimit(urls, 8, (url) => fetch(url).then((r) => r.text()));

Cancellation & timeouts with AbortController {Hủy & timeout với AbortController}

The modern, standard way to cancel async work — supported by fetch, fs, timers, streams, and events {Cách hủy hiện đại, chuẩn — được fetch, fs, timers, stream, event hỗ trợ}:

// Built-in timeout signal (Node 17.3+) — no manual setTimeout bookkeeping
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

// Combine your own cancel + a timeout
const ac = new AbortController();
const signal = AbortSignal.any([ac.signal, AbortSignal.timeout(10_000)]);
button.onClick = () => ac.abort();          // cancel on user action
const data = await fetch(url, { signal });

Error handling that doesn’t lie {Xử lý lỗi không nói dối}

try {
  const data = await risky();
} catch (err: unknown) {
  // In TS, caught errors are `unknown` — narrow before use, never assume Error
  if (err instanceof Error) console.error(err.message, { cause: err.cause });
  else console.error('Non-Error thrown:', err);
}

Two process-level safety nets — log and exit, never swallow {Hai lưới an toàn mức process — log rồi thoát, đừng nuốt lỗi}:

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exitCode = 1;           // let in-flight work drain, then exit non-zero
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  process.exit(1);                // state is corrupt — exit hard, let the orchestrator restart
});

Request-scoped context with AsyncLocalStorage {Context theo request với AsyncLocalStorage}

A senior tool: carry a request id / user through an async call chain without threading it through every function argument {Một công cụ senior: mang request id / user qua chuỗi gọi async mà không phải truyền qua từng tham số hàm}:

import { AsyncLocalStorage } from 'node:async_hooks';

interface RequestContext { requestId: string; userId?: string }
export const als = new AsyncLocalStorage<RequestContext>();

// At the edge (e.g. an HTTP middleware):
als.run({ requestId: crypto.randomUUID() }, () => handle(req, res));

// Anywhere deep inside, with no plumbing:
function log(msg: string): void {
  const ctx = als.getStore();
  console.log(`[${ctx?.requestId}] ${msg}`);
}

4. Modules: ESM vs CommonJS {Module: ESM vs CommonJS}

Node supports two systems, and senior bugs hide in their differences {Node hỗ trợ hai hệ, và bug senior trốn trong khác biệt của chúng}.

Aspect {Khía cạnh}CommonJS (CJS)ES Modules (ESM)
Import {Nhập}const x = require('x')import x from 'x'
Export {Xuất}module.exports = …export / export default
Loading {Tải}synchronous {đồng bộ}asynchronous {bất đồng bộ}
__dirnameavailable {có sẵn}use import.meta.dirname (Node 20+)
Top-level awaitno {không}yes {có}

Pick ESM for new projects — set it once in package.json {Chọn ESM cho dự án mới — đặt một lần trong package.json}:

{
  "type": "module",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}
// ESM essentials you'll use constantly
import { readFile } from 'node:fs/promises';

const here = import.meta.dirname;          // replaces __dirname
const isMain = import.meta.url === `file://${process.argv[1]}`; // "run directly?"

// Dynamic import — load on demand, or conditionally (returns a Promise)
const { default: chalk } = await import('chalk');

Always use the node: prefix for built-ins (node:fs, node:path) {Luôn dùng tiền tố node: cho built-in}: it resolves faster and can never be shadowed by a malicious npm package named fs {nó resolve nhanh hơn và không bao giờ bị một package npm độc tên fs chiếm chỗ}.

TypeScript note {Ghi chú TypeScript}: Node 24 can run .ts files directly via type-stripping (node --experimental-strip-types app.ts, on by default for many cases), and tsx remains the smoothest dev runner {Node 24 chạy .ts trực tiếp qua type-stripping, và tsx vẫn là trình chạy dev mượt nhất}. Type-stripping removes types but does not transform enums or namespaces — keep code “erasable” {Type-stripping bỏ type nhưng không biến đổi enum hay namespace — giữ code “có thể xóa type”}.


5. Core modules — deep dive {Core module — đào sâu}

Master these and you depend on far fewer npm packages {Thành thạo chúng và bạn phụ thuộc ít package npm hơn nhiều}.

fs — filesystem {fs — hệ thống tệp}

import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';

await mkdir('data', { recursive: true });              // mkdir -p, no error if exists
await writeFile('data/out.txt', 'hello', 'utf8');
const entries = await readdir('.', { withFileTypes: true }); // Dirent[] — has isDirectory()
const info = await stat('data/out.txt');               // size, mtime, mode...

Prefer node:fs/promises over the sync variants — readFileSync blocks the entire event loop {Ưu tiên node:fs/promises hơn bản sync — readFileSync chặn cả event loop}. And don’t check-then-act (a TOCTOU race); just try and handle the error code {Và đừng kiểm-tra-rồi-làm (đua TOCTOU); cứ thử và xử lý mã lỗi}:

// ❌ Race: the file can vanish between exists() and readFile()
// ✅ Just attempt it and branch on the error code
async function readOrNull(path: string): Promise<string | null> {
  try {
    return await readFile(path, 'utf8');
  } catch (err) {
    if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return null;
    throw err;                 // ENOENT = not found; anything else is a real problem
  }
}

path & os — never hand-concatenate paths {path & os — đừng tự nối đường dẫn}

import { join, resolve, basename, extname, parse } from 'node:path';
import { cpus, tmpdir, homedir, platform } from 'node:os';

join('data', 'cache', 'file.json'); // cross-platform separators (\ on Windows)
extname('report.pdf');              // '.pdf'
parse('/a/b/c.txt');               // { dir:'/a/b', base:'c.txt', name:'c', ext:'.txt' }
cpus().length;                      // core count → size your worker/cluster pool

events — the EventEmitter pattern {events — mẫu EventEmitter}

Streams, servers, sockets, the process itself — much of Node is built on events {Stream, server, socket, chính process — phần lớn Node xây trên sự kiện}. Type your events for safety {Gõ type cho sự kiện để an toàn}:

import { EventEmitter, once } from 'node:events';

interface JobEvents {
  progress: [percent: number];
  done: [result: string];
}

const job = new EventEmitter<JobEvents>();
job.on('progress', (p) => console.log(`${p}%`));
job.emit('progress', 50);

// Await an event as a Promise (great for "wait until ready")
const [result] = await once(job, 'done');

Three cautions {Ba lưu ý}: (1) the 'error' event is special — emitting it with no listener crashes the process; (2) listeners run synchronously, so a slow listener blocks the emitter; (3) more than 10 listeners on one event logs a memory-leak warning — raise it deliberately with setMaxListeners if intended {(1) sự kiện 'error' đặc biệt — phát mà không có listener sẽ crash process; (2) listener chạy đồng bộ, listener chậm chặn emitter; (3) quá 10 listener trên một sự kiện sẽ cảnh báo rò rỉ bộ nhớ}.

stream — process data too big for memory {stream — xử lý dữ liệu quá lớn cho bộ nhớ}

Reading a 5 GB file into one Buffer kills your process {Đọc file 5 GB vào một Buffer giết process}. Streams process it in chunks with flat memory {Stream xử lý theo chunk với bộ nhớ phẳng}. There are four types {Có bốn loại}: Readable (source), Writable (sink), Duplex (both), Transform (Duplex that modifies, e.g. gzip) {Readable (nguồn), Writable (đích), Duplex (cả hai), Transform (Duplex biến đổi, vd gzip)}.

The senior tool is pipeline, which wires streams together and handles backpressure (pausing a fast source when the slow destination is full) and cleanup automatically {Công cụ senior là pipeline, nối stream và tự xử lý backpressure và dọn dẹp}:

import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';

// Compress a file of ANY size using almost no memory
await pipeline(
  createReadStream('big.log'),
  createGzip(),                  // a Transform stream
  createWriteStream('big.log.gz'),
);

Build your own Transform — e.g. uppercase every line — in object or byte mode {Tự dựng Transform — vd viết hoa mỗi dòng}:

import { Transform } from 'node:stream';

const upper = new Transform({
  transform(chunk, _enc, cb) {
    cb(null, chunk.toString().toUpperCase()); // push transformed chunk downstream
  },
});

Modern Node also speaks the Web Streams API (ReadableStream), and you can convert with Readable.fromWeb / Readable.toWeb — handy when sharing code with the browser or edge runtimes {Node hiện đại còn nói Web Streams API, chuyển đổi bằng Readable.fromWeb / Readable.toWeb}.

buffer — raw bytes, and a security gotcha {buffer — byte thô, và một bẫy bảo mật}

A Buffer is a fixed-length chunk of binary data (a subclass of Uint8Array) {Buffer là một khối dữ liệu nhị phân độ dài cố định (lớp con của Uint8Array)}. Bytes are not characters {Byte không phải ký tự}:

const buf = Buffer.from('café', 'utf8');
console.log(buf.length);          // 5 — 'é' is 2 bytes in UTF-8, not 1

// ✅ Buffer.alloc zero-fills.  ❌ allocUnsafe reuses memory (may leak old data)
const safe = Buffer.alloc(1024);          // zeroed, safe to send anywhere
const fast = Buffer.allocUnsafe(1024);    // faster, but MUST fully overwrite first

util, timers, crypto, process {util, timers, crypto, process}

import { promisify, parseArgs, styleText } from 'node:util';
import { setTimeout as sleep } from 'node:timers/promises';
import { randomUUID, createHash } from 'node:crypto';

await sleep(1000);                          // promise-based delay, no callback
const id = randomUUID();                    // RFC 4122 v4 UUID
const digest = createHash('sha256').update('data').digest('hex');

// Parse CLI flags without a dependency (Node 18.3+)
const { values } = parseArgs({
  options: { top: { type: 'string' }, verbose: { type: 'boolean', short: 'v' } },
});

// process — your window into the runtime
process.argv;        // [node, script, ...args]
process.env.NODE_ENV;
process.cwd();       // current working dir
process.on('SIGTERM', () => server.close()); // graceful shutdown on orchestrator signal

6. Concurrency, parallelism, worker threads & cluster {Đồng thời, song song, worker thread & cluster}

A distinction every senior states cleanly {Một phân biệt mọi senior phải nói rõ}:

  • Concurrencydealing with many things by interleaving on one thread (the event loop’s I/O model) {Đồng thờixử lý nhiều việc bằng đan xen trên một luồng (mô hình I/O của event loop)}.
  • Parallelismdoing many things at the exact same instant on multiple CPU cores {Song songthực thi nhiều việc cùng một thời điểm trên nhiều core CPU}.

For I/O (DBs, APIs, files), the event loop’s concurrency is enough {Với I/O, concurrency của event loop là đủ}. For genuine CPU-bound work (hashing, image resize, parsing huge JSON, crypto), one thread blocks everything — offload to a worker thread {Với việc nặng CPU thật sự, một luồng chặn mọi thứ — đẩy sang worker thread}:

// worker.ts — runs on its own thread, its own V8 isolate + event loop
import { parentPort, workerData } from 'node:worker_threads';
parentPort?.postMessage(heavyCompute(workerData));
// main.ts
import { Worker } from 'node:worker_threads';

function runHeavy(input: unknown): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL('./worker.ts', import.meta.url), { workerData: input });
    worker.once('message', resolve);
    worker.once('error', reject);
    worker.once('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
    });
  });
}
// The main event loop stays responsive while the worker crunches.

Workers vs cluster {Worker vs cluster}: a worker thread shares memory (via SharedArrayBuffer) and is for CPU work within one process {worker thread chia sẻ bộ nhớ và dành cho việc CPU trong một process}. The cluster module forks whole processes that share a listening port, to use all cores for I/O throughput — though in production a process manager (PM2) or the orchestrator usually does this for you (Phase 7–8) {module cluster fork cả process chia sẻ cổng lắng nghe, để dùng hết core cho throughput I/O — dù production thường để PM2 / orchestrator làm thay}.

Senior judgment {Phán đoán senior}: workers add real complexity (serialization cost, memory per isolate) — reserve them for measured CPU bottlenecks, never for I/O {worker thêm phức tạp thật (chi phí serialize, bộ nhớ mỗi isolate) — chỉ dùng cho nút thắt CPU đã đo, đừng dùng cho I/O}.


7. Memory & garbage collection — what seniors watch {Bộ nhớ & thu gom rác — điều senior theo dõi}

V8 manages memory in a heap split into a young generation (short-lived objects, collected often and cheaply) and an old generation (survivors, collected rarely but expensively) {V8 quản lý bộ nhớ trong một heap chia thành young generation (vật thể ngắn hạn, gom thường xuyên và rẻ) và old generation (vật thể sống sót, gom hiếm nhưng đắt)}.

const m = process.memoryUsage();
// rss: total resident memory; heapTotal/heapUsed: V8 heap; external: C++ buffers
console.log(`heapUsed: ${(m.heapUsed / 1024 / 1024).toFixed(1)} MB`);

The default old-space cap is ~2–4 GB depending on version; raise it for memory-heavy jobs {Trần old-space mặc định ~2–4 GB tùy phiên bản; nâng lên cho job nặng bộ nhớ}:

node --max-old-space-size=4096 server.js   # cap at 4 GB

The four classic leak sources {Bốn nguồn rò rỉ kinh điển}: (1) ever-growing module-level arrays/maps used as caches without eviction; (2) forgotten setInterval / event listeners; (3) closures capturing large objects; (4) Map/Set keyed by objects that never get deleted — use WeakMap/WeakRef when the cache should not keep keys alive {(1) mảng/map cấp module phình mãi làm cache không evict; (2) setInterval/listener bị quên; (3) closure bắt giữ vật thể lớn; (4) Map/Set khóa bằng object không bao giờ xóa — dùng WeakMap/WeakRef}.

Rule that prevents most of them {Quy tắc ngăn phần lớn}: stream large data, cap your caches, and clear every timer/listener you create {stream dữ liệu lớn, giới hạn cache, và xóa mọi timer/listener bạn tạo}.


8. Diagnostics you’ll use daily {Chẩn đoán dùng hằng ngày}

node --inspect server.js        # open chrome://inspect or VS Code debugger
node --watch server.js          # built-in restart-on-change (no nodemon needed)
node --env-file=.env server.js  # load .env without dotenv (Node 20.6+)
node --test                     # built-in test runner (Phase 9)
import { performance } from 'node:perf_hooks';

const t0 = performance.now();
await doWork();
console.log(`took ${(performance.now() - t0).toFixed(1)}ms`);

9. Hands-on projects {Dự án thực hành}

Build all three — this is how the concepts stick {Dựng cả ba — đây là cách khái niệm thấm vào}.

  1. Streaming log filter {Bộ lọc log bằng stream}: watch a directory with fs.watch; when a .log file changes, stream-read only appended lines with createReadStream + readline and print those containing ERROR — so memory stays flat on huge logs {theo dõi thư mục bằng fs.watch; khi file .log đổi, stream-đọc chỉ dòng mới bằng createReadStream + readline và in dòng chứa ERROR — bộ nhớ phẳng kể cả log khổng lồ}.

  2. du-style CLI {CLI kiểu du}: build a directory-size command using parseArgs for flags (--top, --ext), walking folders recursively with fs/promises and bounded parallelism via your mapLimit, printing the largest files {dựng lệnh đo dung lượng dùng parseArgs, duyệt đệ quy bằng fs/promises với song song có giới hạn qua mapLimit, in các file lớn nhất}.

  3. EventEmitter logger {Logger dựa trên EventEmitter}: class Logger extends EventEmitter emitting log/error; one listener writes JSON lines to a file via a writable stream, another prints to console. Prove what happens when an 'error' event has no listener {class Logger extends EventEmitter phát log/error; một listener ghi JSON ra file qua writable stream, một listener in console. Chứng minh điều gì xảy ra khi 'error' không có listener}.

Extra drills {Bài tập thêm}: predict the output of a mixed setTimeout/setImmediate/Promise/nextTick script (top-level and inside an I/O callback); convert a callback API to async/await with promisify; offload a CPU-heavy hash loop to a worker thread and confirm the main loop stays responsive with a parallel setInterval heartbeat; write mapLimit and prove it never exceeds the cap {dự đoán output script trộn setTimeout/setImmediate/Promise/nextTick (top-level trong I/O callback); chuyển API callback sang async/await bằng promisify; đẩy vòng hash nặng CPU sang worker thread và xác nhận loop chính vẫn phản hồi; viết mapLimit và chứng minh nó không vượt giới hạn}.


10. Senior checklist {Checklist senior}

  • I can explain the event loop phases and where Promises/nextTick fit {Giải thích được các pha event loop và vị trí của Promise/nextTick}.
  • I never block the loop (no sync I/O, no heavy CPU on the main thread) {Không bao giờ chặn loop}.
  • I default to Promise.all, but cap concurrency for large batches {Mặc định Promise.all, nhưng giới hạn đồng thời cho batch lớn}.
  • I cancel with AbortSignal and never swallow errors {Hủy bằng AbortSignal và không nuốt lỗi}.
  • I stream large data and clean up every timer/listener {Stream dữ liệu lớn và dọn mọi timer/listener}.
  • I reach for worker threads only for measured CPU work {Chỉ dùng worker thread cho việc CPU đã đo}.

What’s next {Phần tiếp theo}

You now own the foundation every senior reasons from: how Node is built, the event loop and microtasks, the libuv thread pool, the full async toolkit (combinators, concurrency control, cancellation, context), modules, the core modules, and the memory model {Giờ bạn nắm nền tảng mà mọi senior suy luận từ đó: Node được dựng thế nào, event loop và microtask, thread pool libuv, bộ công cụ async đầy đủ, module, core module, và mô hình bộ nhớ}.

In Phase 2, we go up one layer to HTTP & web fundamentals — the protocol in depth, building a raw Node.js server with no framework, parsing requests (including file uploads and streaming bodies), routing by hand, and discovering the middleware concept from first principles {Ở Phase 2, ta lên một tầng tới HTTP & nền tảng web — giao thức chuyên sâu, dựng server Node.js thô không framework, parse request (kể cả upload file và body streaming), tự routing, và khám phá khái niệm middleware từ gốc}.