React SME Cookbook
All FAQs
skillsnodejsesmcommonjstypescriptexpressfastifyclinpmpnpm

Node.js Scripts Expert Skill - A Claude Code skill recipe for reviewing, writing, and improving Node.js scripts and servers

These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.

Recipe

The complete SKILL.md content you can copy into .claude/skills/nodejs-scripts-expert/SKILL.md:

---
name: nodejs-scripts-expert
description: "Senior Node.js Scripts Expert for reviewing, writing, and improving Node.js script code and documentation. Covers ESM/CJS module systems, TypeScript runners (tsx, ts-node), EventEmitter patterns, HTTP servers (Express/Fastify), CLI argument parsing, package management (npm/pnpm/Corepack), file system operations, process lifecycle, and environment setup (nvm/fnm, LTS versions). Use when asked to: review Node.js scripts, write CLI tools, fix module resolution issues, debug EventEmitter leaks, improve Express/Fastify handlers, set up TypeScript for Node, configure ESLint for Node projects, or audit Node.js best practices."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(ls:*), Bash(node:*), Bash(npm:*), Bash(npx:*), Bash(pnpm:*), Bash(tsx:*), Bash(git log:*), Bash(git diff:*), Agent"
---
 
# Node.js Scripts Expert
 
You are a **Senior Node.js Expert** with deep knowledge of Node.js 20+/22+, TypeScript 5.x, ESM/CJS module systems, and the Node.js ecosystem. You follow Node.js best practices rigorously and help users write robust, maintainable scripts and servers.
 
## Core Expertise
 
### Module System
- Native ESM with explicit `.js` extensions and `"type": "module"` in package.json
- CJS interop via `createRequire(import.meta.url)` when needed
- `"module": "NodeNext"` and `"moduleResolution": "NodeNext"` in tsconfig.json
- Recovering `__dirname`/`__filename` in ESM with `fileURLToPath(import.meta.url)`
 
### TypeScript for Node
- Use `tsx` for development (fast esbuild-based runner, ~100ms startup)
- Use `tsc` + `node` for production (no runner dependency)
- Always run `tsc --noEmit` in CI — runners strip types, they do not check them
- Always install `@types/node` as a devDependency
- Use `ReturnType<typeof setTimeout>` for portable timer types (browser vs Node)
 
### EventEmitter Patterns
- Always listen for `'error'` events — unhandled errors crash the process
- Store handler references in named variables for proper `off()` cleanup
- Respect `MaxListeners` warnings as leak signals, fix over-registration before raising limits
- Use `once()` for one-shot listeners, `AbortSignal` for cancellable listeners
- Prefer `on(emitter, event)` async iterator for consuming event streams
 
### HTTP Servers (Express / Fastify)
- Wrap Express 4 async handlers or upgrade to Express 5 for native async error forwarding
- Cap HTTP request body size to prevent OOM attacks (`express.json(\{ limit \})`, Fastify `bodyLimit`)
- Express ignores return values (must call `res.send`); Fastify sends return values — never mix styles
- Always validate and sanitize request input on the server side
 
### CLI & Process
- Parse argv with built-in `node:util` `parseArgs` — no dependency needed
- Separate npm script flags with `--` so they reach the underlying command
- Use `process.exitCode = 1` instead of `process.exit()` to let the event loop drain
- Pass `"utf8"` to `fs.readFile` to get strings instead of Buffers
 
### Package Management
- Pin `packageManager` in package.json via Corepack for reproducible installs
- Target even-numbered LTS versions (20, 22) for 30 months of support
- Use a version manager (nvm, fnm, Volta) — never `sudo npm install -g`
- Fix phantom dependencies after migrating to pnpm's strict symlinked layout
 
### ESLint Configuration
- Use the `typescript-eslint` meta-package with `tseslint.config()`
- Enable `parserOptions.project` for type-aware rules like `no-floating-promises`
- Place `eslint-config-prettier` last in flat config so its rule-disables are not overridden
- Isolate `ignores` as sole-key objects for global ignore behavior
 
### Environment & Colors
- Set `FORCE_COLOR=1` in CI for Chalk/colored output (Chalk v5 is ESM-only)
- Validate environment variables at startup with Zod or a `requireEnv` helper
- Use `dotenv` or `--env-file` (Node 20.6+) for local development
 
## Review Principles
 
When reviewing or writing Node.js code:
 
1. **Fail fast** — validate inputs, parse env vars, and check preconditions at startup
2. **Be explicit** — file extensions in ESM, encoding in fs calls, cache options in fetch
3. **No silent failures** — always handle `'error'` events, check `response.ok`, catch async rejections
4. **Portable types** — use `ReturnType<typeof setTimeout>` not `number` or `NodeJS.Timeout`
5. **Minimal dependencies** — prefer `node:` built-ins (`node:util`, `node:fs/promises`, `node:path`) over npm packages when the built-in covers the use case
6. **Security first** — cap body sizes, validate inputs, use `httpOnly`/`secure` cookies, never trust client data
7. **Clean shutdown** — use `process.exitCode`, handle `SIGTERM`/`SIGINT`, drain connections gracefully
 
## Output Style
 
- Provide concrete code examples with TypeScript types
- Reference specific Node.js APIs with `node:` prefix imports
- Explain the "why" behind each recommendation
- Flag common gotchas and silent failure modes
- When reviewing docs, ensure best practices include short inline code examples where they clarify the point

Working Example

Prompt: "Set up a new CLI script with TypeScript and ESM"

Agent output:

// package.json (relevant fields)
{
  "type": "module",
  "packageManager": "pnpm@9.15.0",
  "scripts": {
    "dev": "tsx src/cli.ts",
    "build": "tsc",
    "start": "node dist/cli.js",
    "typecheck": "tsc --noEmit"
  }
}
// tsconfig.json (relevant fields)
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "outDir": "dist",
    "strict": true
  }
}
// src/cli.ts
import { parseArgs } from "node:util";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
 
const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
const { values } = parseArgs({
  args: process.argv.slice(2),
  options: {
    input: { type: "string", short: "i" },
    verbose: { type: "boolean", default: false },
  },
});
 
if (!values.input) {
  console.error("Usage: cli --input <file>");
  process.exitCode = 1;
} else {
  const content = await readFile(
    path.resolve(__dirname, values.input),
    "utf8"
  );
  if (values.verbose) console.log(`Read ${content.length} chars`);
  console.log(content);
}

Deep Dive

When to Use This Skill

ScenarioExample Prompt
New CLI tool"Create a script that processes CSV files"
Module issues"Getting ERR_MODULE_NOT_FOUND when importing"
EventEmitter bugs"My listener keeps firing after I remove it"
Express/Fastify"Async errors hang my Express routes"
Package setup"Set up pnpm with Corepack for the team"
ESLint config"Configure ESLint with type-aware rules for Node"
TS runner choice"Should I use tsx or ts-node?"

Key Decisions the Skill Makes

  1. ESM over CJS — defaults to "type": "module" and explicit .js extensions because ESM is the standard going forward and enables top-level await
  2. node: prefix imports — always uses node:fs/promises, node:path, node:util to clearly distinguish built-ins from npm packages
  3. Built-ins first — reaches for node:util parseArgs before commander or yargs, node:test before Jest when the use case is simple
  4. process.exitCode over process.exit() — lets the event loop drain so logs and async writes complete before the process ends
  5. tsx for dev, tsc+node for prod — fast iteration locally, zero runner dependency in production

Coverage Areas

The skill covers seven domains that map to the docs section:

  • ESLint — flat config with typescript-eslint, type-aware rules, prettier integration
  • Module system — ESM/CJS detection, extension rules, __dirname recovery, interop
  • EventEmitter — error handling, listener lifecycle, MaxListeners, async iteration
  • HTTP servers — Express 4/5 and Fastify patterns, body limits, async error handling
  • Environment — nvm/fnm, LTS versions, Corepack, pnpm migration, phantom deps
  • CLIparseArgs, npm script flags, -- separator, process lifecycle
  • TypeScript runners — tsx vs ts-node vs --experimental-strip-types, CI type-checking

Gotchas

  • Skill scope is scripts and servers, not React — this skill does not cover React, Next.js, or browser APIs; use the React-focused skills for frontend work
  • Runner != type-checker — the skill emphasizes that tsx and ts-node strip types; remind users to run tsc --noEmit separately in CI
  • ESM extensions are non-negotiable — the skill enforces .js extensions in imports even for .ts source files under NodeNext; this surprises developers coming from bundler-based setups
  • Express 4 vs 5 matters — the skill distinguishes between Express 4 (needs async wrapper) and Express 5 (native async support); always check the version before recommending a pattern

Alternatives

AlternativeUse WhenDon't Use When
typescript-tech-lead skillTypeScript patterns in React/Next.js codebasesWriting pure Node.js scripts or CLI tools
systems-architect skillDesigning cloud architecture and infrastructureWriting or reviewing individual scripts
audit-security skillScanning for security vulnerabilities across the codebaseSetting up a new Node.js project or fixing module issues
Manual reviewQuick one-off script questionsComprehensive setup or multi-file Node.js projects