Back to skills
SKILL.md
Nodejs Best Practices
ASecurityUse when Node.js 22+ production mastery. ES Modules, async patterns, Express/Fastify/Hono, middleware architecture, error handling, streaming, worker threads, environment config, security hardening, process management, and deployment patterns. Use when building Node.js servers, APIs, CLI tools, or any server-side JavaScript.
- 5 stars
- 0 votes
- 0 copies
- 0 views
- Added September 27, 2026
Works with
Security analysis
100/100npx -y skills add Harmitx7/tribunal-kit --skill nodejs-best-practices --agent claude-codeAre you the author of Nodejs Best Practices?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harmitx7-nodejs-best-practices-tribunal-kit)---
name: nodejs-best-practices
description: "Use when Node.js 22+ production mastery. ES Modules, async patterns, Express/Fastify/Hono, middleware architecture, error handling, streaming, worker threads, environment config, security hardening, process management, and deployment patterns. Use when building Node.js servers, APIs, CLI tools, or any server-side JavaScript."
version: 5.0.0
last-updated: 2026-09-13
skills:
- backend-security-expert
- api-patterns
- clean-code
tools: Read, Grep, Glob, Bash, Edit, Write
scripts-binding:
- .agent/scripts/lint_runner.js
- .agent/scripts/verify_all.js
---
# Node.js Best Practices β Node 22+ Production Mastery
---
## π οΈ Technical Architecture & Reference Recipes
---
## 2026 Node.js 22+ Performance & Runtime Invariants
1. **`node:sqlite` Built-in Database**: In Node 22+, use `node:sqlite` for embedded storage without requiring compiled native C++ bindings:
```ts
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
```
2. **`import.meta.dirname` & `import.meta.filename`**: Native in Node 20.11+ and 22+:
```ts
import { join } from 'node:path';
const configPath = join(import.meta.dirname, 'config.json');
```
3. **Safe Async Stream Pipelining**: Always use `pipeline` from `node:stream/promises` to automatically destroy streams on error and prevent memory leaks.
4. **Native Test Runner**: Use `node:test` and `node:assert/strict` for zero-dependency high-speed test suites.
## Hallucination Traps (Read First)
- β `import fs from "fs"` β β
`import fs from "node:fs"` (always use `node:` prefix)
- β `path.dirname(fileURLToPath(import.meta.url))` β β
Node 22+: `import.meta.dirname`
- β Installing `better-sqlite3` for basic SQLite β β
Use native `import { DatabaseSync } from 'node:sqlite'`
- β `fs.readFileSync()` inside request handlers β β
`await fs.readFile()` from `node:fs/promises`
- β Unhandled promise rejections without crashing β β
In Node 22+, always handle or exit cleanly
---
## ES Modules (Mandatory)
```json
// package.json β ALWAYS set type to module
{
"type": "module",
"engines": { "node": ">=22" }
}
```
```typescript
// β
ESM imports (modern Node.js)
import { readFile, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { createServer } from 'node:http';
import { EventEmitter } from 'node:events';
// β HALLUCINATION TRAP: Use node: protocol prefix for built-in modules
// β import fs from "fs"; β ambiguous (could be npm package)
// β
import fs from "node:fs"; β explicitly a Node.js built-in
// β HALLUCINATION TRAP: Do NOT use require() in ESM projects
// β const fs = require("fs"); β CommonJS (legacy)
// β
import fs from "node:fs/promises"; β ESM
// Dynamic imports (for conditional loading)
const module = await import('./heavy-module.js');
```
---
## Framework Selection
```
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β When to Use What β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Express β Legacy projects, extensive middleware ecosystemβ
β Fastify β Performance-critical APIs, schema validation β
β Hono β Edge/serverless, multi-runtime (Node/Deno/Bun)β
β tRPC β Full-stack TypeScript (Next.js + React Query) β
β Raw http β Learning, minimal proxies, health checks β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Fastify (Recommended)
```typescript
import Fastify from 'fastify';
import { z } from 'zod';
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV === 'development' ? { target: 'pino-pretty' } : undefined,
},
});
// Schema validation with Zod β Fastify schema
const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user'),
});
type CreateUserBody = z.infer<typeof CreateUserSchema>;
app.post<{ Body: CreateUserBody }>(
'/users',
{
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['admin', 'user'] },
},
},
},
},
async (request, reply) => {
const validated = CreateUserSchema.parse(request.body);
const user = await createUser(validated);
return reply.status(201).send(user);
},
);
// Graceful shutdown
const start = async () => {
try {
await app.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
```
### Hono (Edge-First)
```typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
app.use('*', logger());
app.use('*', cors({ origin: 'https://myapp.com' }));
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
app.post('/users', zValidator('json', createUserSchema), async c => {
const body = c.req.valid('json');
const user = await createUser(body);
return c.json(user, 201);
});
app.get('/health', c => c.json({ status: 'ok' }));
export default app; // works in Node, Deno, Bun, Cloudflare Workers
```
---
## Error Handling
### Global Error Handlers
```typescript
// β
MANDATORY: Handle unhandled rejections and exceptions
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Log to error tracking service (Sentry, etc.)
// Gracefully shutdown
process.exit(1);
});
process.on('uncaughtException', error => {
console.error('Uncaught Exception:', error);
// Log to error tracking service
process.exit(1); // MUST exit β state is corrupted
});
// β HALLUCINATION TRAP: After uncaughtException, the process MUST exit
// The process is in an undefined state β continuing is dangerous
// β process.on("uncaughtException", (err) => { console.log(err); }); // continues running
// β
process.on("uncaughtException", (err) => { log(err); process.exit(1); });
```
### Application Error Classes
```typescript
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = 'INTERNAL_ERROR',
public isOperational: boolean = true,
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} '${id}' not found`, 404, 'NOT_FOUND');
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public errors: Record<string, string[]> = {},
) {
super(message, 400, 'VALIDATION_ERROR');
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401, 'UNAUTHORIZED');
}
}
// Express error middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError && err.isOperational) {
res.status(err.statusCode).json({
error: { code: err.code, message: err.message },
});
} else {
// Programmer error β log and return generic message
console.error('Unexpected error:', err);
res.status(500).json({
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
});
}
});
```
---
## Async Patterns
### Parallel vs Sequential
```typescript
// β SEQUENTIAL: Each await blocks the next
const users = await getUsers(); // 200ms
const posts = await getPosts(); // 200ms
const stats = await getStats(); // 200ms
// Total: 600ms
// β
PARALLEL: All start simultaneously
const [users, posts, stats] = await Promise.all([
getUsers(), // 200ms
getPosts(), // 200ms (concurrent)
getStats(), // 200ms (concurrent)
]);
// Total: ~200ms
// Promise.allSettled β when some can fail
const results = await Promise.allSettled([
fetchCriticalData(),
fetchOptionalData(),
fetchAnalytics(),
]);
for (const result of results) {
if (result.status === 'fulfilled') {
process(result.value);
} else {
console.error('Failed:', result.reason);
}
}
```
### Retry Pattern
```typescript
async function withRetry<T>(
fn: () => Promise<T>,
options: { maxRetries?: number; baseDelay?: number; maxDelay?: number } = {},
): Promise<T> {
const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000 } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
const jitter = delay * (0.5 + Math.random() * 0.5);
console.warn(`Attempt ${attempt + 1} failed, retrying in ${jitter}ms`);
await new Promise(resolve => setTimeout(resolve, jitter));
}
}
throw new Error('Unreachable');
}
// Usage:
const data = await withRetry(() => fetch('https://api.flaky.com/data'), {
maxRetries: 3,
baseDelay: 500,
});
```
### AbortController (Timeouts & Cancellation)
```typescript
async function fetchWithTimeout(url: string, timeoutMs = 5000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return response;
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
```
---
## Streaming
```typescript
import { Readable, Transform, pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
// Stream large file processing (no memory issues)
async function processLargeCSV(inputPath: string, outputPath: string) {
const transform = new Transform({
transform(chunk, encoding, callback) {
const processed = chunk.toString().toUpperCase();
callback(null, processed);
},
});
await pipeline(
createReadStream(inputPath),
transform,
createGzip(),
createWriteStream(outputPath),
);
}
// Streaming HTTP response
app.get('/export', async (req, res) => {
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=export.csv');
const cursor = db.collection('users').find().stream();
cursor.on('data', user => {
res.write(`${user.id},${user.name},${user.email}\n`);
});
cursor.on('end', () => res.end());
cursor.on('error', err => {
console.error(err);
res.status(500).end();
});
});
// β HALLUCINATION TRAP: Use stream/promises (not callbacks)
// β const { pipeline } = require("stream"); β callback-based
// β
import { pipeline } from "node:stream/promises"; β async/await
```
---
## Environment & Config
```typescript
// config.ts β centralized, validated configuration
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
CORS_ORIGIN: z.string().default('http://localhost:5173'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export const config = envSchema.parse(process.env);
// β HALLUCINATION TRAP: Validate env vars at startup β fail FAST
// β process.env.DATABASE_URL! β crashes at runtime, not startup
// β
Validate with Zod on app init β crash immediately with clear error
// β HALLUCINATION TRAP: Never hardcode secrets
// β const JWT_SECRET = "my-secret-key"; β in source code
// β
const JWT_SECRET = process.env.JWT_SECRET; β from environment
```
---
## Security Hardening
```typescript
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
// Security headers
app.use(helmet());
// Rate limiting
app.use(
'/api/',
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, try again later' },
}),
);
// Auth rate limiting (stricter)
app.use(
'/api/auth/',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // only 5 login attempts per 15 min
}),
);
// Input validation (ALWAYS validate)
app.post('/api/users', async (req, res, next) => {
try {
const data = CreateUserSchema.parse(req.body); // Zod validates
const user = await createUser(data);
res.status(201).json(user);
} catch (error) {
next(error);
}
});
// SQL injection prevention (covered by parameterized queries)
// β db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// β
db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
// Path traversal prevention
import { resolve, normalize } from 'node:path';
function safePath(userInput: string, baseDir: string): string {
const resolved = resolve(baseDir, normalize(userInput));
if (!resolved.startsWith(baseDir)) {
throw new Error('Path traversal detected');
}
return resolved;
}
```
---
## Process Management
### Graceful Shutdown
```typescript
async function gracefulShutdown(signal: string) {
console.log(`\n${signal} received β shutting down gracefully`);
// Stop accepting new connections
server.close();
// Close database connections
await db.end();
// Close Redis
await redis.quit();
// Allow in-flight requests 10s to complete
setTimeout(() => {
console.error('Forceful shutdown after timeout');
process.exit(1);
}, 10000);
process.exit(0);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
```
### Worker Threads (CPU-Bound)
```typescript
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
if (isMainThread) {
// Main thread β offload CPU work
function runWorker(data: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL(import.meta.url), { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
const result = await runWorker({ input: largeDataSet });
} else {
// Worker thread β CPU-intensive work
const result = heavyComputation(workerData.input);
parentPort?.postMessage(result);
}
// β HALLUCINATION TRAP: Worker threads are for CPU-bound tasks
// For I/O-bound tasks (network, file), use async/await β NOT workers
// Workers have overhead (serialization, memory) β don't overuse
```
Attribution
Comments
Loading commentsβ¦