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.mdsource 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
) exclusively — setexport
in"type": "module"package.json - Use native TypeScript execution via
(Node 22.6+) or--experimental-strip-types
for developmenttsx - Use
by default,const
only when reassignment is needed, neverletvar - Use
/async
over raw Promises; never use callbacks for new codeawait
Prefer Native APIs Over npm Packages
- Use
instead offetch()
,node-fetch
, oraxiosgot - Use
+node:test
instead of Jest or Mocha for new projectsnode:assert - Use
instead ofnode --watchnodemon - Use
instead ofnode --env-file=.envdotenv - Use
instead ofcrypto.randomUUID()uuid - Use
instead ofstructuredClone()lodash.cloneDeep - Use
instead ofutil.parseArgs()
oryargs
for simple CLIscommander - Use
(global, Node 22+) instead ofWebSocket
when sufficientws - Use
(Node 22+) instead offs.glob()
packageglob - Use
/AbortController
for cancellationAbortSignal - Use
for worker pool sizingnavigator.hardwareConcurrency - Use
,Blob
,File
,FormData
,Response
from global scope (Web API compatible)Request
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
withError
chaining:causeclass 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
andunhandledRejection
— log and exit for programmer errorsuncaughtException - 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
for shutdown deadlinesAbortSignal.timeout() - Close database pools, flush logs, and drain queues before exit
- Use
(Node 18.2+) after a grace periodserver.closeAllConnections()
Performance
- Never block the event loop — offload CPU-heavy work to
worker_threads - Use
andStreams
frompipeline()
for large data processingnode:stream/promises - Use
fromAsyncLocalStorage
for request-scoped context (tracing, logging)node:async_hooks - Use
to yield to the event loop in tight loopssetImmediate() - Use
only when you will fill the buffer immediatelyBuffer.allocUnsafe() - Use connection pooling for databases — never create connections per request
- Use
for production logging (structured JSON, async transport)Pino - Profile with
ornode --prof
+ Chrome DevToolsnode --inspect - Use
for measuring custom metricsperf_hooks
HTTP Server Patterns
- Separate server creation from listening (testability)
- Use
or a framework (Fastify preferred, Express acceptable)http.createServer() - Always set request timeouts:
andserver.setTimeout()server.keepAliveTimeout - Use
ornode:cluster
for multi-process deployment when neededpm2 - Set
to prevent socket leaksserver.headersTimeout > server.keepAliveTimeout
Security
- Use the Node.js Permission Model (
) for sandboxing where applicable--permission - Never use
,eval()
, ornew Function()
with user inputvm.runInContext() - Use
for secret comparisoncrypto.timingSafeEqual() - Sanitize all user inputs — never pass to
unescapedchild_process - Use
middleware for HTTP security headershelmet - Run
in CI/CD — block on critical/high vulnerabilitiesnpm audit - Use
(notnpm ci
) in production and CI buildsnpm install - Pin exact dependency versions for production (
)--save-exact - Use
for hashing, encryption, and random valuesnode:crypto
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
for mocking,mock.method()
for timer controlmock.timers - Use
for coverage reports--experimental-test-coverage - Use
for test-driven developmentnode --test --watch - Run tests with
node --test 'tests/**/*.test.ts' - Use
for additional test outputt.diagnostic() - Use snapshot testing with
(Node 22+)assert.snapshot()
Configuration
- Use
for environment variablesnode --env-file=.env - 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
scattered throughout the codebase — centralize itprocess.env
Docker
- Use multi-stage builds:
for productionnode:22-slim - Run as non-root user:
USER node - Use
directly, notnode
(proper signal handling)npm start - Copy
andpackage.json
first for layer cachingpackage-lock.json - Use
to exclude.dockerignore
,node_modules
, tests.git
Anti-Patterns to Avoid
- ❌ Using
in ESM projectsrequire() - ❌ Using
,node-fetch
,dotenv
,uuid
when native alternatives existnodemon - ❌ Using
in request handlersfs.readFileSync - ❌ Using
without try/catchJSON.parse() - ❌ Storing secrets in code or
package.json - ❌ Using
without cleanupprocess.exit() - ❌ Ignoring backpressure in streams
- ❌ Using
in production (use structured logger)console.log - ❌ Creating god-modules with mixed responsibilities
- ❌ Using
orany
as escape hatches@ts-ignore