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.

June 11, 2026by PromptGenius Team
nodejscursor-rulesjavascriptruntimebackendserver
Node.js Cursor Rules: JavaScript Runtime

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);
});