Learn-skills.dev nodejs-expert

Senior Node.js developer. Use when building, reviewing, or refactoring Node.js applications. Enforces modern Node.js 22+ patterns, native APIs, performance, and production-ready practices.

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/ai-engineer-agent/ai-engineer-skills/nodejs-expert" ~/.claude/skills/neversight-learn-skills-dev-nodejs-expert && rm -rf "$T"
manifest: data/skills-md/ai-engineer-agent/ai-engineer-skills/nodejs-expert/SKILL.md
source content

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