
Nodejs
- 3 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
nodejs is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- nodejs
- AI & Agent Building
- AI-coding skill
Nodejs by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,677 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill nodejsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
| Topic | Reference | Contents |
|---|---|---|
| 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"inpackage.json. Use.mjs/.cjsonly 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"inpackage.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:moduleonly when you mustrequire()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_threadsor 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()`.
nextTickstarves 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/promisesfor 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
tryblocks — preserves full
stack traces and ensures catch fires for rejections.
- Extend `Error`. Custom errors must extend
Error, set acodeproperty 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
helmetor 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-sizeto 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/promisesover callback-basednode:fs. - Prefer
node:stream/promisesfor 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.
{
"sources": {
"Node.js Best Practices (goldbergyoni)": "https://raw.githubusercontent.com/goldbergyoni/nodebestpractices/master/README.md",
"Node.js Packages and Exports": "https://raw.githubusercontent.com/nodejs/node/main/doc/api/packages.md",
"Node.js ESM Documentation": "https://raw.githubusercontent.com/nodejs/node/main/doc/api/esm.md",
"Node.js Errors API": "https://raw.githubusercontent.com/nodejs/node/main/doc/api/errors.md",
"Node.js Security Best Practices": "https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/pages/en/learn/getting-started/security-best-practices.md",
"Node.js Don't Block the Event Loop": "https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/pages/en/learn/asynchronous-work/dont-block-the-event-loop.md",
"Node.js Event Loop Timers and nextTick": "https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/pages/en/learn/asynchronous-work/event-loop-timers-and-nexttick.md",
"Node.js How to Use Streams": "https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/pages/en/learn/modules/how-to-use-streams.md",
"Node.js Backpressuring in Streams": "https://raw.githubusercontent.com/nodejs/nodejs.org/main/apps/site/pages/en/learn/modules/backpressuring-in-streams.md",
"OWASP Node.js Security Cheat Sheet": "https://raw.githubusercontent.com/OWASP/CheatSheetSeries/master/cheatsheets/Nodejs_Security_Cheat_Sheet.md"
},
"lastFetched": "2026-02-16T13:19:39.688Z"
}
Node.js Error Handling
Error classes, error.cause, process-level error events, and error handling patterns.
Error Categories
| Category | Description | Response |
|---|---|---|
| Operational | Bad input, network timeout, file not found | Handle gracefully, respond to caller |
| Programmer | Null deref, assertion failure, type error | Crash and restart — state is unreliable |
This distinction drives your error handling strategy. Operational errors are handled in-place. Programmer errors mean corrupted state — the safest response is to exit and let the process manager restart.
Custom Error Classes
Always extend Error. Set a code property for programmatic matching (not message strings, which change).
class AppError extends Error {
constructor(message, code, options) {
super(message, options); // options.cause for chaining
this.name = 'AppError';
this.code = code;
}
}
class NotFoundError extends AppError {
constructor(resource, options) {
super(`${resource} not found`, 'ERR_NOT_FOUND', options);
this.name = 'NotFoundError';
this.resource = resource;
}
}
class ValidationError extends AppError {
constructor(message, fields, options) {
super(message, 'ERR_VALIDATION', options);
this.name = 'ValidationError';
this.fields = fields;
}
}Usage
// Throwing with cause chain
try {
const data = await fetchFromDB(id);
} catch (err) {
throw new NotFoundError('user', { cause: err });
}
// Matching by code (stable across versions)
catch (err) {
if (err.code === 'ERR_NOT_FOUND') {
res.status(404).json({ error: err.message });
} else {
throw err; // Re-throw unknown errors
}
}
// Matching by class
catch (err) {
if (err instanceof ValidationError) {
res.status(400).json({ error: err.message, fields: err.fields });
} else {
throw err;
}
}Error Cause Chaining
Use error.cause (ES2022) to preserve the original error while adding context:
async function getUser(id) {
try {
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
} catch (err) {
throw new Error(`failed to fetch user ${id}`, { cause: err });
}
}The full chain is visible via util.inspect() and structured loggers. Node.js error codes use error.code (not error.cause) for identification.
Async Error Patterns
Always return await
// BAD — intermediate function disappears from stack trace
async function getUser(id) {
try {
return fetchUser(id); // Missing await!
} catch (err) {
// This catch NEVER fires for fetchUser rejections
throw new AppError('user fetch failed', 'ERR_FETCH', { cause: err });
}
}
// GOOD — full stack trace, catch block works
async function getUser(id) {
try {
return await fetchUser(id);
} catch (err) {
throw new AppError('user fetch failed', 'ERR_FETCH', { cause: err });
}
}Centralized Error Handler
Don't scatter error handling across every middleware. Use a centralized error handler:
// Express-style centralized error handler
function errorHandler(err, req, res, next) {
logger.error({ err, reqId: req.id }, err.message);
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message, fields: err.fields });
}
if (err instanceof NotFoundError) {
return res.status(404).json({ error: err.message });
}
// Unknown error — don't leak internals
res.status(500).json({ error: 'Internal server error' });
}Process-Level Error Events
unhandledRejection
Fires when a Promise rejects and no handler is attached. Always register this.
process.on('unhandledRejection', (reason, promise) => {
logger.error({ err: reason }, 'Unhandled promise rejection');
// In production: log, clean up, exit
process.exit(1);
});Since Node.js 15+, unhandled rejections throw by default (--unhandled-rejections=throw). The process crashes if you don't handle them.
uncaughtException
Fires when a synchronous throw escapes all try/catch blocks. Log and exit.
process.on('uncaughtException', (err, origin) => {
logger.fatal({ err, origin }, 'Uncaught exception — shutting down');
// Do NOT continue running — state is unreliable
process.exit(1);
});Never try to resume after uncaughtException. The process state is unknown.
warning
Non-fatal process warnings (deprecations, memory leaks, experimental features):
process.on('warning', (warning) => {
logger.warn({ warning }, warning.message);
});EventEmitter Error Events
All EventEmitters (streams, servers, sockets) emit 'error' events. An unhandled 'error' event crashes the process.
// BAD — crashes if connection fails
const connection = net.connect('localhost:5432');
// GOOD
const connection = net.connect('localhost:5432');
connection.on('error', (err) => {
logger.error({ err }, 'Connection failed');
});Streams
Streams are EventEmitters. Always handle their 'error' event unless using pipeline() (which handles it automatically):
// pipeline handles errors — no manual handler needed
await pipeline(readable, transform, writable);
// Manual pipe — MUST handle errors on each stream
readable.on('error', handleError);
writable.on('error', handleError);
readable.pipe(writable);Graceful Shutdown
When a fatal error occurs, clean up before exiting:
async function gracefulShutdown(signal) {
logger.info(`Received ${signal}, shutting down gracefully`);
// 1. Stop accepting new connections
server.close();
// 2. Wait for in-flight requests (with timeout)
const timeout = setTimeout(() => {
logger.error('Graceful shutdown timed out, forcing exit');
process.exit(1);
}, 30_000);
try {
// 3. Close database connections, flush logs, etc.
await db.end();
await logger.flush();
clearTimeout(timeout);
process.exit(0);
} catch (err) {
logger.error({ err }, 'Error during shutdown');
process.exit(1);
}
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));Anti-Patterns
| Don't | Do |
|---|---|
throw "string" | throw new Error("message") |
Catch and ignore: catch (e) {} | Handle, log, or re-throw with cause |
catch (e) { log(e); throw e } | Handle once: log OR throw, not both |
Resume after uncaughtException | Log, cleanup, exit |
| Match errors by message string | Match by error.code or instanceof |
| Nest try/catch deeply | Centralized error handler |
| Swallow errors in event handlers | Always re-emit or log |
return promise in try block | return await promise |
Node.js Event Loop
Event loop phases, timers, microtasks, and non-blocking patterns.
Architecture
Node.js uses a single-threaded event loop for JavaScript execution and a libuv worker pool for expensive I/O and CPU tasks.
┌───────────────────────────┐
┌─>│ timers │ setTimeout, setInterval callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ System-level callbacks (TCP errors, etc.)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ Internal only
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ I/O callbacks, incoming connections
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ setImmediate callbacks
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ socket.on('close', ...)
└───────────────────────────┘Between every phase transition, Node.js drains the microtask queue: 1. process.nextTick() callbacks (highest priority) 2. Promise .then()/catch()/finally() callbacks
Execution Priority
process.nextTick() → Runs before ANY other queued work (between phases)
Promise microtasks → Runs after nextTick, before next phase
setTimeout(fn, 0) → Timers phase (minimum ~1ms delay)
setImmediate(fn) → Check phase (after poll)
I/O callbacks → Poll phaseKey Insight
process.nextTick() starves the event loop if called recursively. Prefer setImmediate() for deferring work to the next iteration:
// BAD — starves I/O, timers never fire
function recursiveNext() {
process.nextTick(recursiveNext);
}
// GOOD — yields to I/O between iterations
function recursiveImmediate() {
setImmediate(recursiveImmediate);
}Use process.nextTick() only when you need to run something before any I/O in the current tick (e.g., emitting events after construction but before the caller can attach listeners).
Don't Block the Event Loop
The event loop is shared across all clients. Blocking it blocks everyone.
What Blocks
| Operation | Impact | Fix |
|---|---|---|
fs.readFileSync() | Blocks on disk I/O | await fs.readFile() |
child_process.execSync() | Blocks on subprocess | child_process.exec() |
crypto.pbkdf2Sync() | CPU-bound | crypto.pbkdf2() (async) |
zlib.inflateSync() | CPU-bound | zlib.inflate() (async) |
JSON.parse(hugeString) | O(n) CPU | Limit input size, stream parse |
JSON.stringify(hugeObj) | O(n) CPU | Limit depth, stream serialize |
| Vulnerable regex | O(2^n) CPU | Use safe-regex, RE2, or indexOf |
Tight while loop | CPU-bound | Break into chunks with setImmediate |
Partitioning CPU Work
For CPU-bound tasks that must run on the main thread, partition into chunks:
function processChunk(items, index, chunkSize, callback) {
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// process items[i]
}
if (end < items.length) {
setImmediate(() => processChunk(items, end, chunkSize, callback));
} else {
callback();
}
}For heavy computation, prefer worker_threads:
import { Worker, isMainThread, parentPort } from 'node:worker_threads';
if (isMainThread) {
const worker = new Worker(import.meta.filename);
worker.on('message', (result) => console.log(result));
worker.postMessage({ data: heavyInput });
} else {
parentPort.on('message', (msg) => {
const result = expensiveComputation(msg.data);
parentPort.postMessage(result);
});
}Worker Pool (libuv Thread Pool)
These Node.js APIs use the libuv thread pool (default 4 threads, configurable via UV_THREADPOOL_SIZE, max 1024):
I/O-intensive:
dns.lookup(),dns.lookupService()- All
fsasync operations (exceptfs.FSWatcher)
CPU-intensive:
crypto.pbkdf2(),crypto.scrypt(),crypto.randomBytes(),crypto.randomFill()- All
zlibasync operations
If your app makes heavy use of these, increase UV_THREADPOOL_SIZE:
UV_THREADPOOL_SIZE=16 node server.jsRule of thumb: set it to at least the number of concurrent I/O operations you expect.
Timers
setTimeout vs setImmediate
setTimeout(fn, 0)— fires in the timers phase (next loop iteration, ~1ms minimum)setImmediate(fn)— fires in the check phase (after poll)
Inside an I/O callback, setImmediate always fires before setTimeout(fn, 0):
import fs from 'node:fs';
fs.readFile('/dev/null', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Output: immediate, timeout (always in this order)Outside I/O, the order is non-deterministic. Don't depend on it.
AbortController for Cancellable Timers
const ac = new AbortController();
setTimeout(() => {
console.log('This may not run');
}, 5000, undefined, { signal: ac.signal });
// Cancel the timer
ac.abort();queueMicrotask vs process.nextTick
Both run before the next event loop phase, but:
process.nextTick | queueMicrotask | |
|---|---|---|
| Queue | nextTick queue | microtask queue (with Promises) |
| Priority | Runs first | Runs after all nextTick callbacks |
| Standard | Node.js-specific | Web standard (cross-platform) |
| Starvation risk | Higher (recursive calls block I/O) | Same risk, but standard |
Prefer queueMicrotask() for new code — it's cross-platform and standard. Use process.nextTick() only when you need to guarantee execution before any I/O or Promise callbacks.
Node.js Module System
ESM/CJS configuration, package.json fields, exports, imports, and module resolution.
ESM Is the Default
All new Node.js projects should use ESM. Set the package type explicitly:
{
"type": "module"
}This makes all .js files in the package parse as ES modules. Without this field, .js files default to CommonJS (legacy behavior).
File Extensions
| Extension | Always parsed as | Regardless of "type" |
|---|---|---|
.mjs | ES module | Yes |
.cjs | CommonJS | Yes |
.js | Depends on "type" | No |
Use .mjs/.cjs only when you need to mix module systems within a single package.
node: Protocol
Always prefix built-in module imports with node::
// Correct
import fs from 'node:fs/promises';
import { createServer } from 'node:http';
import { pipeline } from 'node:stream/promises';
// Wrong — ambiguous, could collide with npm packages
import fs from 'fs';Benefits:
- Unambiguous — clearly a built-in, not an npm package
- Prevents package name collision attacks
- Required for some newer built-in modules
package.json Fields
"type"
Controls how .js files are interpreted:
| Value | .js parsed as |
|---|---|
"module" | ES module |
"commonjs" (or absent) | CommonJS |
Always set this field explicitly, even in CommonJS packages — future-proofs the package and helps tooling.
"exports" (Recommended for Libraries)
Defines the public API surface. Prevents consumers from importing internal files.
{
"name": "my-lib",
"type": "module",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}Key behaviors:
- Encapsulation: only paths listed in
"exports"are importable - Supersedes `"main"`: when both exist,
"exports"wins - Targets must start with `./`: relative URLs only
Conditional Exports
Serve different code depending on how the package is loaded:
{
"exports": {
".": {
"import": "./src/index.js",
"require": "./src/index.cjs",
"default": "./src/index.js"
}
}
}Condition priority (most specific first): node-addons > node > import > require
module-sync>default.
Always include "default" as a fallback.
TypeScript Types Condition
When publishing TypeScript declarations, place "types" first:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}"imports" (Private Mappings)
Package-internal import aliases. Must start with #:
{
"imports": {
"#db": {
"node": "./src/db-node.js",
"default": "./src/db-polyfill.js"
},
"#utils/*": "./src/utils/*.js"
}
}Usage:
import { connect } from '#db';
import { slugify } from '#utils/string';Benefits:
- Clean internal paths without
../../../ - Conditional resolution (platform-specific implementations)
- No package self-reference needed
"main" (Legacy)
Single entry point, no encapsulation. Use "exports" instead for new packages. Keep "main" alongside "exports" only for backward compatibility with old Node.js or bundlers:
{
"main": "./src/index.js",
"exports": "./src/index.js"
}ESM vs CommonJS Differences
| Feature | ESM | CommonJS |
|---|---|---|
| Syntax | import/export | require()/module.exports |
| Loading | Async | Sync |
__dirname | Not available | Available |
__filename | Not available | Available |
import.meta.dirname | Available (Node 21.2+) | Not available |
import.meta.filename | Available (Node 21.2+) | Not available |
require.main | Not available | Available |
import.meta.main | Available (Node 24.2+) | Not available |
| JSON import | Needs with { type: 'json' } | require('./data.json') |
| Top-level await | Supported | Not supported |
| Live bindings | Yes | No (value copies) |
Replacing CJS Patterns in ESM
// __dirname / __filename replacement
import.meta.dirname // '/Users/me/project/src'
import.meta.filename // '/Users/me/project/src/index.js'
// require.resolve replacement
import.meta.resolve('some-package') // 'file:///path/to/some-package/index.js'
// require() in ESM (when absolutely needed)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const nativeAddon = require('./addon.node');
// JSON import
import config from './config.json' with { type: 'json' };
// Dynamic import (works in both ESM and CJS)
const module = await import('./dynamic-module.js');Interoperability
Importing CJS from ESM
// Default import always works
import lodash from 'lodash';
// Named imports work if CJS module uses static export patterns
import { readFile } from 'node:fs';
// When named imports fail, destructure the default
import pkg from 'some-cjs-package';
const { helper } = pkg;Using ESM from CJS
require() can load synchronous ESM (no top-level await):
// Works if the ESM module has no top-level await
const { something } = require('esm-package');For ESM with top-level await, use dynamic import():
async function main() {
const { something } = await import('esm-package');
}Self-Referencing
A package can import its own exports by name:
{
"name": "my-package",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js"
}
}// Inside my-package itself
import { helper } from 'my-package/utils';Requires "exports" to be defined.
Node.js Security
Input validation, dependency supply chain, DoS prevention, and secure defaults.
Input Validation
Validate Everything from Outside
Never trust user input — request bodies, query params, headers, file uploads, environment variables from untrusted sources.
// Use a schema validation library (zod, ajv, typebox)
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
function createUser(input) {
const user = UserSchema.parse(input); // Throws on invalid input
return db.insert(user);
}Request Size Limits
Always limit request body size. Unbounded payloads exhaust memory:
// Express
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
// Fastify (built-in)
const app = fastify({ bodyLimit: 1_048_576 }); // 1 MBSet different limits per content type. JSON parsing is more expensive than multipart.
JSON Parsing DoS
JSON.parse() is O(n) but blocks the event loop for large inputs. A 50MB JSON string takes ~2 seconds to parse.
Mitigations:
- Limit payload size at the HTTP layer (before parsing)
- For very large JSON, use streaming parsers (
stream-json,@streamparser/json) - Validate
Content-Lengthheader before reading the body
Regex Denial of Service (ReDoS)
Vulnerable regex patterns can take exponential time on crafted input, blocking the event loop.
Dangerous Patterns
- Nested quantifiers:
(a+)*,(a+)+,(a*)* - Overlapping alternations:
(a|a)*,(a|ab)* - Backreferences with repetition:
(a.*)\1
Mitigations
- Use `indexOf`/`includes` for simple string matching — always O(n)
- Validate regex with tools like
safe-regex2orrecheck - Use RE2 via
re2npm package for untrusted patterns (linear time guarantee) - Bound input length before regex matching
// BAD — exponential on crafted input
const VULNERABLE = /(\/.+)+$/;
filePath.match(VULNERABLE);
// GOOD — bounded, no nested quantifiers
const SAFE = /^(\/[^/]+)+$/;
if (filePath.length < 1000) filePath.match(SAFE);Dependency Supply Chain
Threats
| Attack | Description |
|---|---|
| Typosquatting | Package with similar name to popular one (lodsah vs lodash) |
| Compromised maintainer | Attacker gains publish access to legitimate package |
| Lockfile poisoning | Modified lockfile points to malicious version |
| Dependency confusion | Private package name claimed on public registry |
| Malicious postinstall | Package runs arbitrary code on npm install |
Mitigations
- Use `npm ci` in CI and production — enforces lockfile exactly, fails on mismatch
- Lock dependencies — commit
package-lock.jsonoryarn.lock - Pin exact versions for critical dependencies (no
^or~) - Audit regularly —
npm auditin CI pipeline - Disable postinstall scripts for untrusted packages:
npm install --ignore-scripts- Review before install — check package size, maintainers, dependencies
- Use `npm publish --dry-run` to verify what gets published
- Configure `.npmignore` or
"files"inpackage.jsonto prevent secret leaks - Enable 2FA on npm accounts
HTTP Security
Server Timeouts
Always configure timeouts on HTTP servers. Defaults are too permissive:
import { createServer } from 'node:http';
const server = createServer(handler);
server.headersTimeout = 60_000; // Max time to receive headers
server.requestTimeout = 60_000; // Max time for entire request
server.timeout = 120_000; // Socket inactivity timeout
server.keepAliveTimeout = 5_000; // Keep-alive socket timeout
server.maxRequestsPerSocket = 100; // Limit requests per connectionSecurity Headers
Use helmet (Express) or equivalent for security headers:
Strict-Transport-Security— enforce HTTPSX-Content-Type-Options: nosniff— prevent MIME sniffingX-Frame-Options: DENY— prevent clickjackingContent-Security-Policy— control resource loading
Delegate TLS/gzip to Reverse Proxy
Node.js should not terminate TLS or compress responses in production. Let nginx, HAProxy, or a cloud load balancer handle:
- TLS termination — CPU-intensive, optimized in native code
- gzip/brotli compression — blocks event loop for large responses
- Rate limiting — better at infrastructure layer
- Request filtering — WAF rules
Secrets Management
- Never hardcode secrets in source code
- Use environment variables loaded from secure vaults (not
.envin production) - Never commit `.env` files — add to
.gitignore - Use `crypto.timingSafeEqual()` for secret comparison (prevents timing attacks):
import { timingSafeEqual } from 'node:crypto';
function verifyToken(provided, expected) {
if (provided.length !== expected.length) return false;
return timingSafeEqual(
Buffer.from(provided),
Buffer.from(expected)
);
}- Use `crypto.scrypt()` or `crypto.pbkdf2()` for password hashing (async versions)
Child Processes
Avoid Shell Injection
import { execFile, spawn } from 'node:child_process';
// BAD — shell injection via user input
exec(`ls ${userInput}`);
// GOOD — arguments passed as array, no shell interpretation
execFile('ls', [userInput]);
spawn('ls', [userInput]);Never use exec() with user-controlled strings. Use execFile() or spawn() with argument arrays.
Avoid eval and Dynamic require
// BAD — arbitrary code execution
eval(userInput);
new Function(userInput);
require(userInput); // Module loading as code execution
// These are security vulnerabilities, not patternsProcess Hardening
- Run as non-root — use a dedicated user in Docker:
RUN addgroup --system app && adduser --system --ingroup app app
USER app- Set `NODE_ENV=production` — disables debug output, enables optimizations
- Use `--secure-heap` for sensitive crypto operations (Linux only):
node --secure-heap=4096 server.js- Limit V8 heap to prevent memory exhaustion:
node --max-old-space-size=512 server.jsSecurity Checklist
For every HTTP endpoint:
- [ ] Input validated against schema (reject unknown fields)
- [ ] Request body size limited
- [ ] No user input in
exec(),eval(), or dynamicrequire() - [ ] Error responses don't leak stack traces or internals
- [ ] Authentication/authorization checked before processing
For every deployment:
- [ ] Dependencies audited (
npm audit) - [ ] Lockfile committed and enforced (
npm ci) - [ ] Secrets in environment variables, not code
- [ ] Running as non-root
- [ ] TLS terminated at reverse proxy
- [ ] Server timeouts configured
- [ ]
NODE_ENV=production
Node.js Streams
Stream types, pipeline, backpressure, and practical patterns.
Why Streams
Streams process data incrementally in chunks rather than loading everything into memory. Use streams when:
- Reading/writing large files
- Processing HTTP request/response bodies
- Transforming data pipelines (compress, encrypt, parse)
- Proxying data between sources
Don't use streams when data is already fully in memory — the overhead isn't worth it.
Stream Types
| Type | Purpose | Example |
|---|---|---|
Readable | Source of data | fs.createReadStream(), http.IncomingMessage |
Writable | Destination for data | fs.createWriteStream(), http.ServerResponse |
Duplex | Both readable and writable | net.Socket, zlib streams |
Transform | Duplex that modifies data | zlib.createGzip(), custom parsers |
pipeline() — The Only Way to Compose Streams
Always use pipeline() from node:stream/promises. Never use .pipe() directly.
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('input.txt'),
createGzip(),
createWriteStream('input.txt.gz')
);Why Not .pipe()
.pipe() does not:
- Propagate errors from source to destination
- Clean up streams on error (causes resource leaks)
- Return a promise
// BAD — error in gzip silently drops, readStream leaks
readStream.pipe(gzip).pipe(writeStream);
// GOOD — errors propagate, all streams cleaned up
await pipeline(readStream, gzip, writeStream);Backpressure
Backpressure occurs when a writable stream can't consume data as fast as the readable produces it. Ignoring backpressure causes unbounded memory growth.
How It Works
1. writable.write(chunk) returns false when the internal buffer exceeds highWaterMark 2. The producer must stop writing and wait for the 'drain' event 3. pipeline() handles this automatically
Manual Backpressure Handling
When writing to a stream without pipeline():
async function writeData(writable, chunks) {
for (const chunk of chunks) {
const canContinue = writable.write(chunk);
if (!canContinue) {
await new Promise((resolve) => writable.once('drain', resolve));
}
}
writable.end();
}highWaterMark
Buffer threshold in bytes (or objects in object mode). Defaults:
- 16 KiB for byte streams
- 16 objects for object mode streams
It's a hint, not a hard limit. Streams can buffer beyond it.
Creating Custom Streams
Readable
import { Readable } from 'node:stream';
// From async generator (preferred)
async function* generateData() {
for (let i = 0; i < 100; i++) {
yield `line ${i}\n`;
}
}
const readable = Readable.from(generateData());
// Class-based (when you need more control)
class MyReadable extends Readable {
#index = 0;
_read(size) {
if (this.#index >= 100) {
this.push(null); // Signal end
return;
}
this.push(`line ${this.#index++}\n`);
}
}Writable
import { Writable } from 'node:stream';
class MyWritable extends Writable {
_write(chunk, encoding, callback) {
// Process chunk, then signal completion
process.stdout.write(chunk);
callback(); // or callback(err) on failure
}
_final(callback) {
// Called after all data written, before 'finish' event
callback();
}
}Transform
import { Transform } from 'node:stream';
class UpperCase extends Transform {
_transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
}
_flush(callback) {
// Called at the end — emit any remaining data
callback();
}
}Object Mode
Streams default to byte/string chunks. Object mode allows arbitrary JS objects:
const objectStream = new Transform({
objectMode: true,
transform(obj, encoding, callback) {
callback(null, { ...obj, processed: true });
},
});Object mode streams count objects (not bytes) against highWaterMark.
Async Iteration
Readable streams are async iterables:
import { createReadStream } from 'node:fs';
const stream = createReadStream('data.txt', { encoding: 'utf8' });
for await (const chunk of stream) {
process.stdout.write(chunk);
}This is the simplest way to consume a readable stream. Backpressure is handled automatically.
Common Patterns
File Copy with Progress
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { Transform } from 'node:stream';
const { size } = await stat('source.bin');
let transferred = 0;
const progress = new Transform({
transform(chunk, enc, cb) {
transferred += chunk.length;
process.stderr.write(`\r${((transferred / size) * 100).toFixed(1)}%`);
cb(null, chunk);
},
});
await pipeline(
createReadStream('source.bin'),
progress,
createWriteStream('dest.bin')
);HTTP Response Streaming
import { pipeline } from 'node:stream/promises';
import { createReadStream } from 'node:fs';
async function handler(req, res) {
res.setHeader('Content-Type', 'application/octet-stream');
await pipeline(createReadStream('large-file.zip'), res);
}Line-by-Line Processing
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('log.txt'),
crlfDelay: Infinity,
});
for await (const line of rl) {
// Process line
}Error Handling in Streams
- Always handle `'error'` events on streams not managed by
pipeline().
An unhandled 'error' event on a stream crashes the process.
- `pipeline()` handles errors automatically — cleans up all streams and rejects
the promise.
- Destroy streams explicitly when you need to abort:
stream.destroy(new Error('aborted'));- Never ignore the `'error'` event on EventEmitters:
// BAD — crashes process on error
const stream = createReadStream('maybe-missing.txt');
// GOOD
const stream = createReadStream('maybe-missing.txt');
stream.on('error', (err) => {
console.error('Stream error:', err.message);
});