Node.js Cursor Rules: JavaScript Runtime
Cursor rules for Node.js covering ESM vs CommonJS, event loop, streams, Buffer, error handling, cluster/worker threads, file system, security, and package.json conventions.

Overview
Node.js is the dominant server-side JavaScript runtime, powering millions of APIs, CLIs, and full-stack applications. These cursor rules enforce ESM adoption, async/await patterns, stream-based I/O, proper error handling, security hardening, cluster/worker thread usage, and package.json script conventions so AI assistants generate performant, maintainable Node.js code.
Note:
Enforces ESM imports (import/export), async/await over callbacks, stream pipelines, uncaughtException handling, cluster for multi-core, Buffer safety, .env hygiene, and security headers with helmet.
Rules Configuration
---
description: Enforces Node.js best practices including ESM over CJS, async/await, stream pipelines, error handling, cluster/worker threads, security hardening, and package.json conventions.
globs: **/*.js,**/*.ts,**/*.mjs,**/*.cjs,package.json,.env,.nvmrc
---
# Node.js Best Practices
You are an expert in Node.js, server-side JavaScript, and backend development.
You understand the event loop, stream processing, error handling, process management, and production deployment patterns.
### Module System (ESM vs CJS)
- Prefer ESM syntax: `import/export` over `require/module.exports`
- Use `.mjs` extension or `"type": "module"` in package.json for ESM
- Use `import.meta.url` instead of `__dirname`/`__filename` in ESM
- Use `import.meta.dirname` and `import.meta.filename` (Node 22+) as direct replacements
- When CJS is required, use `.cjs` extension for clarity
- Avoid mixing ESM and CJS in the same package
- Top-level await works in ESM modules without async wrappers
### Async Patterns
- Use async/await exclusively — never raw callbacks
- Promisify legacy APIs: `import { readFile } from "node:fs/promises"`
- Use `Promise.all()` for independent parallel operations
- Use `Promise.allSettled()` when partial failures are acceptable
- Never mix `await` with `.then()`/`.catch()` in the same function
- Handle promise rejections: always have a top-level catch or try/catch
### Event Loop & Performance
- Never block the event loop with synchronous I/O or heavy computation
- Offload CPU-intensive work to worker_threads or child_process
- Use `process.nextTick()` for deferring to the next tick (rare — prefer setImmediate)
- Understand phases: timers → pending callbacks → idle/prepare → poll → check → close callbacks
- Watch for starved timers in long-running poll phases
- Use `--inspect` for Chrome DevTools debugging
### Streams
- Use stream pipelines: `import { pipeline } from "node:stream/promises"`
- Pipe readable → transform → writable with `pipeline(source, transform, destination)`
- Handle backpressure: use `stream.pause()` / `stream.resume()` when not using pipeline
- Prefer `for await (const chunk of readable)` for consuming streams
- Use `Readable.from()` for creating streams from arrays/iterables
- Use `Transform` streams for data transformation without loading entirely in memory
- Always handle stream errors: attach `.on("error", handler)` or wrap in try/catch
### Buffer & Binary Data
- Use `Buffer.from(string, "utf-8")` to create buffers
- Never use `new Buffer()` (deprecated)
- Use `Buffer.alloc(size)` for zero-filled buffers, `Buffer.allocUnsafe(size)` when initializing immediately
- Convert: `buffer.toString("hex")`, `buffer.toString("base64")`
- Use `TextEncoder`/`TextDecoder` for string ↔ Uint8Array conversion (web-standard)
### Error Handling
- Use a global uncaughtException handler: `process.on("uncaughtException", (err) => { log.fatal(err); process.exit(1) })`
- Use `process.on("unhandledRejection", handler)` for unhandled promise rejections
- Create custom error classes extending `Error`: `class AppError extends Error { constructor(message, code) { super(message); this.code = code } }`
- Never throw strings — always throw Error instances with stack traces
- Use `AbortController` / `AbortSignal` for cancellable async operations
- In Express/Fastify: use centralized error-handling middleware
### Cluster & Worker Threads
- Use `cluster` module to fork workers per CPU core: `cluster.fork()` for load balancing
- Use `worker_threads` for CPU-bound tasks: `new Worker("./task.js")`
- Pass data via `worker.postMessage(data)` and listen with `parentPort.on("message", ...)`
- Use `SharedArrayBuffer` for shared memory between threads
- Workers share server ports via IPC when using `cluster`
### File System
- Use `node:fs/promises` for async file operations
- Use streams for large files: `createReadStream` / `createWriteStream`
- Check existence with `fs.access(path)` rather than `fs.existsSync`
- Use `path.join()` and `path.resolve()` — never hardcode `/` or `\`
- Use `path.extname()`, `path.basename()`, `path.dirname()` for path manipulation
- Set proper file permissions: `fs.chmod(path, 0o600)` for sensitive files
### Security
- Use `helmet` middleware for HTTP security headers
- Rate-limit endpoints with `express-rate-limit` or similar
- Never use `eval()`, `new Function()`, or `vm.runInNewContext()` with user input
- Validate all user input — never trust `req.body`, `req.query`, or `req.params`
- Use `crypto.randomUUID()` for generating unique identifiers
- Hash passwords with `crypto.scrypt()` or `bcrypt` — never plain text
- Store secrets in environment variables, never in code
- Use `--require ./tracing.js` for OpenTelemetry instrumentation in production
### Environment & Configuration
- Use `.env` files with `dotenv` for local development, never committ
- Access env vars: `process.env.DATABASE_URL`
- Set `NODE_ENV=production` in production, `NODE_ENV=development` locally
- Use `process.exit(1)` for fatal errors, `process.exit(0)` for clean shutdown
- Graceful shutdown: `process.on("SIGTERM", async () => { await server.close(); process.exit(0) })`
### Package.json Conventions
- Define scripts: `"start": "node server.js"`, `"dev": "node --watch server.js"`
- Use `"engines": { "node": ">=22" }` to declare minimum Node version
- Use `.nvmrc` with the version number for nvm/fnm users
- Include `"private": true` for applications (not libraries)
Installation
Create nodejs.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
# Check your Node.js version
node --version # >= 22 recommended
# Use nvm to manage versions
nvm install 22
nvm use 22
Examples
// server.js — ESM HTTP server with graceful shutdown
import { createServer } from "node:http";
import { pipeline } from "node:stream/promises";
import { createReadStream } from "node:fs";
const PORT = process.env.PORT || 3000;
const server = createServer(async (req, res) => {
try {
if (req.url === "/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
return;
}
if (req.url === "/large-file") {
res.writeHead(200, { "content-type": "application/octet-stream" });
await pipeline(createReadStream("./data.bin"), res);
return;
}
res.writeHead(404);
res.end("Not Found");
} catch (err) {
res.writeHead(500);
res.end("Internal Server Error");
}
});
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
// Graceful shutdown
process.on("SIGTERM", async () => {
console.log("SIGTERM received. Closing server...");
server.close(() => process.exit(0));
});
// worker.js — Worker thread for CPU-intensive task
import { parentPort } from "node:worker_threads";
parentPort.on("message", (data) => {
const result = heavyComputation(data);
parentPort.postMessage(result);
});
function heavyComputation(n) {
let sum = 0;
for (let i = 0; i < n; i++) sum += Math.sqrt(i);
return sum;
}
// errors.js — Custom error handling
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.name = "AppError";
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, "NOT_FOUND", 404);
}
}
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
process.exit(1);
});
Related Resources
Related Articles
Rust Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Rust development enforcing ownership patterns, type safety, async/await practices, and clean code principles with AI assistance for production-ready code.
PyTorch Cursor Rules: Deep Learning Best Practices
Cursor rules for PyTorch covering tensor device management, nn.Module patterns, DataLoader pipelines, training loop conventions, gradient handling, GPU/CUDA optimization, model persistence, and distributed training with DDP.
Jetpack Compose Cursor Rules: Modern Android UI
Cursor rules for Jetpack Compose covering composable functions, state management, Material 3, Navigation, ViewModel, recomposition, and preview testing.