Node.js
Respect the event loop. Every blocking operation is a scalability bug.
Node.js rewards async-first, stream-oriented code. If your Node.js code fights the event loop, it's wrong.
References
- Module system — [
${CLAUDE_SKILL_DIR}/references/modules.md]: ESM/CJS comparison tables, file extension rules,
conditional exports patterns
- Event loop — [
${CLAUDE_SKILL_DIR}/references/event-loop.md]: Phase order, execution priority, blocking
operations table, worker pool
- Streams — [
${CLAUDE_SKILL_DIR}/references/streams.md]: Stream types table, pipeline patterns, backpressure
details
- Error handling — [
${CLAUDE_SKILL_DIR}/references/errors.md]: Error categories table, global handlers,
centralized error handling
- Security — [
${CLAUDE_SKILL_DIR}/references/security.md]: Supply chain threats table, HTTP security headers,
process hardening
Module System
- Use ESM. Set
"type": "module" in package.json. Use .mjs/.cjs only when mixing module systems within one
package.
- Use
node: prefix for all built-in imports: import fs from 'node:fs'. Prevents package name collision attacks
and is unambiguous.
- Import
process explicitly. import process from 'node:process'. Never rely on the process global — explicit
imports make dependencies visible.
- Define
"exports" in package.json for libraries. Encapsulates internals — only paths listed in "exports" are
importable by consumers.
- Imports at module top. No dynamic
import() for statically-known dependencies.
- Use
import.meta.dirname/import.meta.filename instead of __dirname/__filename.
- Use
import.meta.resolve() instead of require.resolve() in ESM.
- Always set
"type" explicitly in package.json, even in CJS packages — future-proofs the package and helps
tooling.
- Use conditional exports for dual CJS/ESM packages. Order:
types > import > require > default. Always
include "default" as fallback.
- Place
"types" first in conditional exports when publishing TypeScript declarations.
- Use
# imports ("imports" in package.json) for clean internal paths without ../../../. Supports conditional
resolution for platform-specific implementations.
- Keep
"main" alongside "exports" only for backward compatibility with old Node.js or bundlers. Never use
"main" alone for new packages.
- JSON imports in ESM require
with { type: 'json' } attribute.
- Use
createRequire() from node:module only when you must require() in ESM (e.g., native addons).
- CJS interop: default import from CJS always works. Named imports work if CJS uses static export patterns;
otherwise destructure the default.
require() can load synchronous ESM (no top-level await). For ESM with top-level await, use dynamic
import().
- Self-referencing: a package can import its own exports by name when
"exports" is defined.
File extension rules and ESM vs CJS comparison tables: see ${CLAUDE_SKILL_DIR}/references/modules.md.
Event Loop
Node.js uses a single-threaded event loop for JavaScript and a libuv worker pool for expensive I/O and CPU tasks.
Core Rules
- Never block the event loop. No sync I/O in servers (
readFileSync, execSync, crypto.pbkdf2Sync,
zlib.inflateSync). Sync APIs are acceptable only in CLI scripts, startup code, or build tools.
- Offload CPU-intensive work to
worker_threads or child processes. For main-thread CPU work, partition into chunks
with setImmediate() between iterations.
- Bound input sizes. Unbounded
JSON.parse, JSON.stringify, regex, or iteration = DoS vector. A 50MB JSON string
blocks the loop for ~2 seconds.
- Avoid vulnerable regex. No nested quantifiers
(a+)*, no overlapping alternations (a|a)*, no backreferences
with repetition. Use safe-regex2, RE2, or indexOf.
- Prefer
setImmediate() over recursive process.nextTick(). nextTick starves I/O if called recursively. Use
nextTick only when you must run before any I/O in the current tick (e.g., emitting events after construction before
listeners attach).
- Prefer
queueMicrotask() over process.nextTick() for new code — it's cross-platform and web-standard.
- Inside I/O callbacks,
setImmediate always fires before setTimeout(fn, 0). Outside I/O, the order is
non-deterministic — do not depend on it.
- Use
AbortController for cancellable timers and operations.
Phase order, execution priority, blocking operations table, and worker pool details: see
${CLAUDE_SKILL_DIR}/references/event-loop.md.
Streams
Streams process data incrementally — use them for large files, HTTP bodies, data transformation pipelines, and proxying.
Do not use streams when data is already fully in memory.
Core Rules
- Use
pipeline() from node:stream/promises for stream composition. Never manual .pipe() chains — they don't
propagate errors or handle cleanup.
- Respect backpressure. Check
.write() return value; wait for 'drain' event before continuing. pipeline()
handles this automatically.
- Prefer
Readable.from() for converting iterables/async iterables to streams.
- Use async iteration (
for await (const chunk of stream)) as the simplest way to consume readable streams.
Backpressure is handled automatically.
- Use
readline with createInterface for line-by-line file processing.
highWaterMark defaults to 16 KiB for byte streams, 16 objects for object mode. It's a hint, not a hard limit.
- Object mode streams count objects (not bytes) against
highWaterMark. Enable with { objectMode: true }.
- Destroy streams explicitly when you need to abort:
stream.destroy(new Error('msg')).
- Custom Readable: prefer async generators with
Readable.from() over class-based _read() implementation unless
you need fine-grained control.
- Custom Transform: implement
_transform(chunk, encoding, callback) and optionally _flush(callback) for
end-of-stream processing.
- Custom Writable: implement
_write(chunk, encoding, callback) and optionally _final(callback) for cleanup
before 'finish' event.
Stream types table and .pipe() pitfalls: see ${CLAUDE_SKILL_DIR}/references/streams.md.
Error Handling
Core Rules
- Use
async/await with try/catch. No callbacks for new code.
- Always
return await when returning promises from try blocks — preserves full stack traces and ensures catch
fires for rejections.
- Extend
Error. Custom errors must extend Error, set a code property for programmatic matching (not message
strings, which change), and set name.
- Use
error.cause for chaining: new Error("context", { cause: originalErr }). The full chain is visible via
util.inspect() and structured loggers.
- Match errors by
error.code or instanceof, never by message string.
- Register global handlers. Always handle
process.on('unhandledRejection') and process.on('uncaughtException').
Log, clean up, exit. Since Node.js 15+, unhandled rejections crash the process by default.
- Never resume after
uncaughtException. The process state is unknown — log, cleanup, exit.
- Subscribe to
'error' events on all EventEmitters and streams. An unhandled 'error' event crashes the process.
pipeline() handles stream errors automatically.
- Handle
process.on('warning') for non-fatal process warnings (deprecations, memory leaks, experimental features).
- Use centralized error handlers. Don't scatter error handling across every middleware. Use a single error handler
that maps error types to HTTP responses without leaking internals.
- Handle once: log OR throw, not both.
catch (e) { log(e); throw e } causes duplicate logging.
- Never swallow errors in event handlers — always re-emit or log.
Error categories table and operational vs programmer error strategies: see ${CLAUDE_SKILL_DIR}/references/errors.md.
Process Lifecycle
- Graceful shutdown. Handle
SIGTERM/SIGINT: stop accepting connections, wait for in-flight requests (with
timeout), close DB pools, flush logs, then process.exit(0). Force-exit on timeout.
- Log to stdout/stderr. Let infrastructure (Docker, systemd) handle log routing. Use structured JSON logging (pino,
winston) in production.
- Set
NODE_ENV=production in production. It enables framework optimizations and disables debug output.
- Use
npm ci in CI/production. Never npm install — it ignores lockfile mismatches.
Security
Input Validation
- Validate everything from outside — request bodies, query params, headers, file uploads, environment variables from
untrusted sources. Use schema validation (zod, ajv, typebox).
- Limit request body size at the HTTP layer before parsing. Set per-content-type limits. Unbounded payloads exhaust
memory.
- Validate
Content-Length header before reading the body.
- Use streaming JSON parsers (
stream-json, @streamparser/json) for very large JSON payloads.
HTTP Security
- Configure server timeouts. Defaults are too permissive. Set
headersTimeout, requestTimeout, timeout,
keepAliveTimeout, maxRequestsPerSocket.
- Use security headers (via
helmet or equivalent): Strict-Transport-Security, X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Content-Security-Policy.
- Delegate TLS/gzip to reverse proxy. Node.js should not terminate TLS or compress responses in production — let
nginx/HAProxy/cloud LB handle TLS termination, compression, rate limiting, and WAF rules.
Secrets & Process Hardening
- Never hardcode secrets in source code. Use environment variables from secure vaults.
- Never commit
.env files — add to .gitignore.
- Use
crypto.timingSafeEqual() for secret comparison (prevents timing attacks).
- Use
crypto.scrypt() or crypto.pbkdf2() (async versions) for password hashing.
- Avoid shell injection. Never use
exec() with user-controlled strings. Use execFile() or spawn() with
argument arrays.
- Never use
eval(), new Function(), or dynamic require() with user input.
- Run as non-root in Docker — use a dedicated user.
- Limit V8 heap with
--max-old-space-size to prevent memory exhaustion.
Supply chain threats table, dependency auditing, and security checklist: see
${CLAUDE_SKILL_DIR}/references/security.md.
Application
When writing Node.js code:
- Apply all conventions silently — don't narrate each rule being followed.
- If an existing codebase contradicts a convention, follow the codebase and flag the divergence once.
- Prefer
node:fs/promises over callback-based node:fs.
- Prefer
node:stream/promises for pipeline operations.
When reviewing Node.js code:
- Cite the specific violation and show the fix inline.
- Don't lecture or quote the rule — state what's wrong and how to fix it.
Integration
The javascript skill governs language choices; this skill governs Node.js runtime decisions. Activate typescript
alongside both when working with TypeScript.
Respect the event loop. When in doubt, make it async.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: xobotyi-cc-foundry-nodejs3description: Node.js4---56# Node.js78**Respect the event loop. Every blocking operation is a scalability bug.**910Node.js rewards async-first, stream-oriented code. If your Node.js code fights the event loop, it's wrong.1112## References1314- **Module system** — [`${CLAUDE_SKILL_DIR}/references/modules.md`]: ESM/CJS comparison tables, file extension rules,15 conditional exports patterns16- **Event loop** — [`${CLAUDE_SKILL_DIR}/references/event-loop.md`]: Phase order, execution priority, blocking17 operations table, worker pool18- **Streams** — [`${CLAUDE_SKILL_DIR}/references/streams.md`]: Stream types table, pipeline patterns, backpressure19 details20- **Error handling** — [`${CLAUDE_SKILL_DIR}/references/errors.md`]: Error categories table, global handlers,21 centralized error handling22- **Security** — [`${CLAUDE_SKILL_DIR}/references/security.md`]: Supply chain threats table, HTTP security headers,23 process hardening2425## Module System2627- **Use ESM.** Set `"type": "module"` in `package.json`. Use `.mjs`/`.cjs` only when mixing module systems within one28 package.29- **Use `node:` prefix** for all built-in imports: `import fs from 'node:fs'`. Prevents package name collision attacks30 and is unambiguous.31- **Import `process` explicitly.** `import process from 'node:process'`. Never rely on the `process` global — explicit32 imports make dependencies visible.33- **Define `"exports"` in `package.json`** for libraries. Encapsulates internals — only paths listed in `"exports"` are34 importable by consumers.35- **Imports at module top.** No dynamic `import()` for statically-known dependencies.36- **Use `import.meta.dirname`/`import.meta.filename`** instead of `__dirname`/`__filename`.37- **Use `import.meta.resolve()`** instead of `require.resolve()` in ESM.38- **Always set `"type"` explicitly** in `package.json`, even in CJS packages — future-proofs the package and helps39 tooling.40- **Use conditional exports** for dual CJS/ESM packages. Order: `types` > `import` > `require` > `default`. Always41 include `"default"` as fallback.42- **Place `"types"` first** in conditional exports when publishing TypeScript declarations.43- **Use `#` imports** (`"imports"` in `package.json`) for clean internal paths without `../../../`. Supports conditional44 resolution for platform-specific implementations.45- **Keep `"main"` alongside `"exports"`** only for backward compatibility with old Node.js or bundlers. Never use46 `"main"` alone for new packages.47- **JSON imports in ESM** require `with { type: 'json' }` attribute.48- **Use `createRequire()`** from `node:module` only when you must `require()` in ESM (e.g., native addons).49- **CJS interop:** default import from CJS always works. Named imports work if CJS uses static export patterns;50 otherwise destructure the default.51- **`require()` can load synchronous ESM** (no top-level `await`). For ESM with top-level `await`, use dynamic52 `import()`.53- **Self-referencing:** a package can import its own exports by name when `"exports"` is defined.5455File extension rules and ESM vs CJS comparison tables: see `${CLAUDE_SKILL_DIR}/references/modules.md`.5657## Event Loop5859Node.js uses a single-threaded event loop for JavaScript and a libuv worker pool for expensive I/O and CPU tasks.6061### Core Rules6263- **Never block the event loop.** No sync I/O in servers (`readFileSync`, `execSync`, `crypto.pbkdf2Sync`,64 `zlib.inflateSync`). Sync APIs are acceptable only in CLI scripts, startup code, or build tools.65- **Offload CPU-intensive work** to `worker_threads` or child processes. For main-thread CPU work, partition into chunks66 with `setImmediate()` between iterations.67- **Bound input sizes.** Unbounded `JSON.parse`, `JSON.stringify`, regex, or iteration = DoS vector. A 50MB JSON string68 blocks the loop for ~2 seconds.69- **Avoid vulnerable regex.** No nested quantifiers `(a+)*`, no overlapping alternations `(a|a)*`, no backreferences70 with repetition. Use `safe-regex2`, RE2, or `indexOf`.71- **Prefer `setImmediate()` over recursive `process.nextTick()`.** `nextTick` starves I/O if called recursively. Use72 `nextTick` only when you must run before any I/O in the current tick (e.g., emitting events after construction before73 listeners attach).74- **Prefer `queueMicrotask()`** over `process.nextTick()` for new code — it's cross-platform and web-standard.75- **Inside I/O callbacks, `setImmediate` always fires before `setTimeout(fn, 0)`.** Outside I/O, the order is76 non-deterministic — do not depend on it.77- **Use `AbortController`** for cancellable timers and operations.7879Phase order, execution priority, blocking operations table, and worker pool details: see80`${CLAUDE_SKILL_DIR}/references/event-loop.md`.8182## Streams8384Streams process data incrementally — use them for large files, HTTP bodies, data transformation pipelines, and proxying.85Do not use streams when data is already fully in memory.8687### Core Rules8889- **Use `pipeline()`** from `node:stream/promises` for stream composition. Never manual `.pipe()` chains — they don't90 propagate errors or handle cleanup.91- **Respect backpressure.** Check `.write()` return value; wait for `'drain'` event before continuing. `pipeline()`92 handles this automatically.93- **Prefer `Readable.from()`** for converting iterables/async iterables to streams.94- **Use async iteration** (`for await (const chunk of stream)`) as the simplest way to consume readable streams.95 Backpressure is handled automatically.96- **Use `readline` with `createInterface`** for line-by-line file processing.97- **`highWaterMark`** defaults to 16 KiB for byte streams, 16 objects for object mode. It's a hint, not a hard limit.98- **Object mode streams** count objects (not bytes) against `highWaterMark`. Enable with `{ objectMode: true }`.99- **Destroy streams explicitly** when you need to abort: `stream.destroy(new Error('msg'))`.100- **Custom Readable:** prefer async generators with `Readable.from()` over class-based `_read()` implementation unless101 you need fine-grained control.102- **Custom Transform:** implement `_transform(chunk, encoding, callback)` and optionally `_flush(callback)` for103 end-of-stream processing.104- **Custom Writable:** implement `_write(chunk, encoding, callback)` and optionally `_final(callback)` for cleanup105 before `'finish'` event.106107Stream types table and `.pipe()` pitfalls: see `${CLAUDE_SKILL_DIR}/references/streams.md`.108109## Error Handling110111### Core Rules112113- **Use `async`/`await` with `try`/`catch`.** No callbacks for new code.114- **Always `return await`** when returning promises from `try` blocks — preserves full stack traces and ensures `catch`115 fires for rejections.116- **Extend `Error`.** Custom errors must extend `Error`, set a `code` property for programmatic matching (not message117 strings, which change), and set `name`.118- **Use `error.cause`** for chaining: `new Error("context", { cause: originalErr })`. The full chain is visible via119 `util.inspect()` and structured loggers.120- **Match errors by `error.code` or `instanceof`**, never by message string.121- **Register global handlers.** Always handle `process.on('unhandledRejection')` and `process.on('uncaughtException')`.122 Log, clean up, exit. Since Node.js 15+, unhandled rejections crash the process by default.123- **Never resume after `uncaughtException`.** The process state is unknown — log, cleanup, exit.124- **Subscribe to `'error'` events** on all EventEmitters and streams. An unhandled `'error'` event crashes the process.125 `pipeline()` handles stream errors automatically.126- **Handle `process.on('warning')`** for non-fatal process warnings (deprecations, memory leaks, experimental features).127- **Use centralized error handlers.** Don't scatter error handling across every middleware. Use a single error handler128 that maps error types to HTTP responses without leaking internals.129- **Handle once: log OR throw, not both.** `catch (e) { log(e); throw e }` causes duplicate logging.130- **Never swallow errors** in event handlers — always re-emit or log.131132Error categories table and operational vs programmer error strategies: see `${CLAUDE_SKILL_DIR}/references/errors.md`.133134## Process Lifecycle135136- **Graceful shutdown.** Handle `SIGTERM`/`SIGINT`: stop accepting connections, wait for in-flight requests (with137 timeout), close DB pools, flush logs, then `process.exit(0)`. Force-exit on timeout.138- **Log to stdout/stderr.** Let infrastructure (Docker, systemd) handle log routing. Use structured JSON logging (pino,139 winston) in production.140- **Set `NODE_ENV=production`** in production. It enables framework optimizations and disables debug output.141- **Use `npm ci`** in CI/production. Never `npm install` — it ignores lockfile mismatches.142143## Security144145### Input Validation146147- **Validate everything from outside** — request bodies, query params, headers, file uploads, environment variables from148 untrusted sources. Use schema validation (zod, ajv, typebox).149- **Limit request body size** at the HTTP layer before parsing. Set per-content-type limits. Unbounded payloads exhaust150 memory.151- **Validate `Content-Length` header** before reading the body.152- **Use streaming JSON parsers** (`stream-json`, `@streamparser/json`) for very large JSON payloads.153154### HTTP Security155156- **Configure server timeouts.** Defaults are too permissive. Set `headersTimeout`, `requestTimeout`, `timeout`,157 `keepAliveTimeout`, `maxRequestsPerSocket`.158- **Use security headers** (via `helmet` or equivalent): `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`,159 `X-Frame-Options: DENY`, `Content-Security-Policy`.160- **Delegate TLS/gzip to reverse proxy.** Node.js should not terminate TLS or compress responses in production — let161 nginx/HAProxy/cloud LB handle TLS termination, compression, rate limiting, and WAF rules.162163### Secrets & Process Hardening164165- **Never hardcode secrets** in source code. Use environment variables from secure vaults.166- **Never commit `.env` files** — add to `.gitignore`.167- **Use `crypto.timingSafeEqual()`** for secret comparison (prevents timing attacks).168- **Use `crypto.scrypt()` or `crypto.pbkdf2()`** (async versions) for password hashing.169- **Avoid shell injection.** Never use `exec()` with user-controlled strings. Use `execFile()` or `spawn()` with170 argument arrays.171- **Never use `eval()`, `new Function()`, or dynamic `require()`** with user input.172- **Run as non-root** in Docker — use a dedicated user.173- **Limit V8 heap** with `--max-old-space-size` to prevent memory exhaustion.174175Supply chain threats table, dependency auditing, and security checklist: see176`${CLAUDE_SKILL_DIR}/references/security.md`.177178## Application179180When **writing** Node.js code:181182- Apply all conventions silently — don't narrate each rule being followed.183- If an existing codebase contradicts a convention, follow the codebase and flag the divergence once.184- Prefer `node:fs/promises` over callback-based `node:fs`.185- Prefer `node:stream/promises` for pipeline operations.186187When **reviewing** Node.js code:188189- Cite the specific violation and show the fix inline.190- Don't lecture or quote the rule — state what's wrong and how to fix it.191192## Integration193194The **javascript** skill governs language choices; this skill governs Node.js runtime decisions. Activate **typescript**195alongside both when working with TypeScript.196197**Respect the event loop. When in doubt, make it async.**198199---200> Converted and distributed by [TomeVault](https://tomevault.io/claim/xobotyi) — claim your Tome and manage your conversions.201<!-- tomevault:4.0:skill_md:2026-04-13 -->