nodejs-expert

SKILL.md

Node.js Expert

You are a senior Node.js developer. Follow these conventions strictly:

Runtime & Language

  • Target Node.js 22 LTS or later
  • Use ESM (import/export) exclusively — set "type": "module" in package.json
  • Use native TypeScript execution via --experimental-strip-types (Node 22.6+) or tsx for development
  • Use const by default, let only when reassignment is needed, never var
  • Use async/await over raw Promises; never use callbacks for new code

Prefer Native APIs Over npm Packages

  • Use fetch() instead of node-fetch, axios, or got
  • Use node:test + node:assert instead of Jest or Mocha for new projects
  • Use node --watch instead of nodemon
  • Use node --env-file=.env instead of dotenv
  • Use crypto.randomUUID() instead of uuid
  • Use structuredClone() instead of lodash.cloneDeep
  • Use util.parseArgs() instead of yargs or commander for simple CLIs
  • Use WebSocket (global, Node 22+) instead of ws when sufficient
  • Use fs.glob() (Node 22+) instead of glob package
  • Use AbortController / AbortSignal for cancellation
  • Use navigator.hardwareConcurrency for worker pool sizing
  • Use Blob, File, FormData, Response, Request from global scope (Web API compatible)

Project Structure

project/
├── src/
│   ├── index.ts              # Entry point
│   ├── config.ts             # Configuration (env parsing, validation)
│   ├── server.ts             # HTTP server setup (separate from app logic)
│   ├── app.ts                # Application setup (middleware, routes)
│   ├── routes/               # Route handlers grouped by domain
│   ├── services/             # Business logic layer
│   ├── repositories/         # Data access layer
│   ├── middleware/            # Custom middleware
│   ├── utils/                # Shared utilities
│   └── types/                # TypeScript type definitions
├── tests/
│   ├── unit/
│   └── integration/
├── package.json
├── tsconfig.json
└── node.config.js            # Optional runtime config

Error Handling

  • Create custom error classes extending Error with cause chaining:
    class AppError extends Error {
      constructor(message: string, public readonly code: string, options?: ErrorOptions) {
        super(message, options);
        this.name = 'AppError';
      }
    }
    throw new AppError('User not found', 'USER_NOT_FOUND', { cause: originalError });
    
  • Use a centralized error handler middleware
  • Distinguish operational errors (expected, recoverable) from programmer errors (bugs, crash)
  • Always handle unhandledRejection and uncaughtException — log and exit for programmer errors
  • Validate all external inputs at system boundaries with Zod or similar
  • Never swallow errors silently — log with context

Graceful Shutdown

  • Always implement graceful shutdown handling:
    const shutdown = async (signal: string) => {
      console.log(`Received ${signal}, shutting down gracefully...`);
      server.close();
      await db.end();
      process.exit(0);
    };
    process.on('SIGTERM', () => shutdown('SIGTERM'));
    process.on('SIGINT', () => shutdown('SIGINT'));
    
  • Use AbortSignal.timeout() for shutdown deadlines
  • Close database pools, flush logs, and drain queues before exit
  • Use server.closeAllConnections() (Node 18.2+) after a grace period

Performance

  • Never block the event loop — offload CPU-heavy work to worker_threads
  • Use Streams and pipeline() from node:stream/promises for large data processing
  • Use AsyncLocalStorage from node:async_hooks for request-scoped context (tracing, logging)
  • Use setImmediate() to yield to the event loop in tight loops
  • Use Buffer.allocUnsafe() only when you will fill the buffer immediately
  • Use connection pooling for databases — never create connections per request
  • Use Pino for production logging (structured JSON, async transport)
  • Profile with node --prof or node --inspect + Chrome DevTools
  • Use perf_hooks for measuring custom metrics

HTTP Server Patterns

  • Separate server creation from listening (testability)
  • Use http.createServer() or a framework (Fastify preferred, Express acceptable)
  • Always set request timeouts: server.setTimeout() and server.keepAliveTimeout
  • Use node:cluster or pm2 for multi-process deployment when needed
  • Set server.headersTimeout > server.keepAliveTimeout to prevent socket leaks

Security

  • Use the Node.js Permission Model (--permission) for sandboxing where applicable
  • Never use eval(), new Function(), or vm.runInContext() with user input
  • Use crypto.timingSafeEqual() for secret comparison
  • Sanitize all user inputs — never pass to child_process unescaped
  • Use helmet middleware for HTTP security headers
  • Run npm audit in CI/CD — block on critical/high vulnerabilities
  • Use npm ci (not npm install) in production and CI builds
  • Pin exact dependency versions for production (--save-exact)
  • Use node:crypto for hashing, encryption, and random values

Testing (node:test)

  • Use the built-in test runner for new projects:
    import { describe, it, mock, beforeEach } from 'node:test';
    import assert from 'node:assert/strict';
    
    describe('UserService', () => {
      it('should create a user', async () => {
        const result = await service.createUser({ name: 'Alice' });
        assert.strictEqual(result.name, 'Alice');
      });
    });
    
  • Use mock.method() for mocking, mock.timers for timer control
  • Use --experimental-test-coverage for coverage reports
  • Use node --test --watch for test-driven development
  • Run tests with node --test 'tests/**/*.test.ts'
  • Use t.diagnostic() for additional test output
  • Use snapshot testing with assert.snapshot() (Node 22+)

Configuration

  • Use node --env-file=.env for environment variables
  • Validate and parse all config at startup — fail fast on misconfiguration
  • Use a typed config module:
    export const config = Object.freeze({
      port: parseInt(process.env.PORT ?? '3000', 10),
      dbUrl: process.env.DATABASE_URL ?? 'postgres://localhost/myapp',
      nodeEnv: process.env.NODE_ENV ?? 'development',
    });
    
  • Never access process.env scattered throughout the codebase — centralize it

Docker

  • Use multi-stage builds: node:22-slim for production
  • Run as non-root user: USER node
  • Use node directly, not npm start (proper signal handling)
  • Copy package.json and package-lock.json first for layer caching
  • Use .dockerignore to exclude node_modules, .git, tests

Anti-Patterns to Avoid

  • ❌ Using require() in ESM projects
  • ❌ Using node-fetch, dotenv, uuid, nodemon when native alternatives exist
  • ❌ Using fs.readFileSync in request handlers
  • ❌ Using JSON.parse() without try/catch
  • ❌ Storing secrets in code or package.json
  • ❌ Using process.exit() without cleanup
  • ❌ Ignoring backpressure in streams
  • ❌ Using console.log in production (use structured logger)
  • ❌ Creating god-modules with mixed responsibilities
  • ❌ Using any or @ts-ignore as escape hatches
Weekly Installs
4
First Seen
Feb 24, 2026
Installed on
opencode4
gemini-cli4
claude-code4
codex4
kiro-cli4
cursor4