| name | nodejs-best-practices |
| description | Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
| version | 1.0.0 |
| last-updated | "2026-03-12T00:00:00.000Z" |
| applies-to-model | gemini-2.5-pro, claude-3-7-sonnet |
Node.js Development Principles
Node.js is not a problem — unpredictable async behavior is.
Understand the event loop and half of Node.js "gotchas" disappear.
Framework Selection
| Context | Recommended | Why |
|---|
| REST API, standard patterns | Express + TypeScript | Mature, flexible, most hiring knowledge |
| REST API, speed + TypeScript-first | Fastify | 2x Express throughput, built-in validation |
| Full-stack React | Next.js API routes | Colocated API and UI, serverless-friendly |
| REST + tRPC | Next.js + tRPC | Type-safe end-to-end with no code generation |
| RPC across services | gRPC | Binary protocol, contract-first |
| Edge function / Cloudflare Worker | Hono | Tiny, Web Platform API-native, zero cold start |
| Scripts / tooling / bun-native | Bun | Built-in bundler, test runner, near-Node compat |
Questions to ask before choosing:
- Is this public-facing or internal?
- Do we control the deployment environment (server vs. serverless vs. edge)?
- Is the team already familiar with a framework?
- Does this need to compose with an existing TypeScript frontend?
Modern Runtime Landscape (2025+)
The Node.js monopoly is ending. Understand constraints before picking a runtime:
| Runtime | fs | crypto | child_process | process.env | Deploy Target |
|---|
| Node.js | ✅ | ✅ | ✅ | ✅ | Server, serverless |
| Bun | ✅ | ✅ | ✅ | ✅ | Server (Node-compatible) |
| Deno | ✅ (explicit perm) | ✅ | ✅ (explicit perm) | ✅ | Server, Deno Deploy |
| Edge (Cloudflare Workers) | ❌ | Web only | ❌ | via env binding | Edge (global) |
import { createHash } from 'crypto';
import fs from 'fs';
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
const response = await fetch('https://api.example.com/data');
When to Consider Bun
Async Patterns
Always: Handle rejection
Every Promise needs a rejection handler. Unhandled rejections crash Node.js processes in modern versions.
fetchData().then(process);
fetchData().then(process).catch(handleError);
try {
const data = await fetchData();
process(data);
} catch (err) {
handleError(err);
}
Avoid: Blocking the event loop
The event loop is single-threaded. Anything synchronous that takes more than a few ms blocks all other requests.
const data = fs.readFileSync('huge.csv');
const data = await fs.promises.readFile('huge.csv');
const result = computeHuge(dataset);
const { Worker } = require('worker_threads');
Concurrency vs. Parallelism
for (const id of ids) {
await processItem(id);
}
await Promise.all(ids.map(id => processItem(id)));
import pLimit from 'p-limit';
const limit = pLimit(5);
await Promise.all(ids.map(id => limit(() => processItem(id))));
Error Handling Architecture
Structure errors so they carry meaning, not just messages:
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number,
public isOperational = true
) {
super(message);
this.name = this.constructor.name;
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
}
}
class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_FAILED', 400);
}
}
Global error handler:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError && err.isOperational) {
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
});
}
logger.error('Unexpected error', { err, url: req.url });
res.status(500).json({ error: 'Internal server error' });
});
Security Baseline
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
app.use(helmet());
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
}));
app.use(express.json({ limit: '10kb' }));
app.set('trust proxy', 1);
Never:
- Use
eval() or new Function() with user input
- Pass unvalidated user input to
exec(), spawn(), or child_process
- Log full request bodies (may contain credentials or PII)
Project Structure
src/
routes/ HTTP route definitions (thin — only parse and delegate)
controllers/ Request handling, validation, response formatting
services/ Business logic (no HTTP awareness)
repositories/ Database access (no business logic)
middleware/ Auth, rate limit, logging
lib/ Shared utilities (date, crypto, validation)
types/ TypeScript interfaces and type exports
config/ Environment config with validation
Dependency direction: routes → controllers → services → repositories
Never: repositories calling services, or services knowing about HTTP
Output Format
When this skill produces or reviews code, structure your output as follows:
━━━ Nodejs Best Practices Report ━━━━━━━━━━━━━━━━━━━━━━━━
Skill: Nodejs Best Practices
Language: [detected language / framework]
Scope: [N files · N functions]
─────────────────────────────────────────────────
✅ Passed: [checks that passed, or "All clean"]
⚠️ Warnings: [non-blocking issues, or "None"]
❌ Blocked: [blocking issues requiring fix, or "None"]
─────────────────────────────────────────────────
VBC status: PENDING → VERIFIED
Evidence: [test output / lint pass / compile success]
VBC (Verification-Before-Completion) is mandatory.
Do not mark status as VERIFIED until concrete terminal evidence is provided.
🏛️ Tribunal Integration (Anti-Hallucination)
Slash command: /tribunal-backend
Active reviewers: logic · security · dependency · type-safety
❌ Forbidden AI Tropes in Node.js
- Blindly mixing
require and import — pick ESM or CommonJS and stick to it strictly based on package.json.
- "Catch-all and ignore" error handling — e.g.,
catch (e) { console.log(e); } without throwing or returning an error response.
- Assuming Express — if the project is Next.js, Fastify, or NestJS, do not hallucinate Express code.
- Unparameterized queries — never interpolate strings into SQL.
- No
any types — unless an external library leaves no choice, type all request bodies and responses.
✅ Pre-Flight Self-Audit
Review these questions before generating Node.js code:
✅ Did I use the correct module system (CJS vs ESM) for this context?
✅ Is every Promise rejection properly handled?
✅ Did I block the event loop with synchronous FS or Crypto operations?
✅ Are all inputs validated before business logic runs?
✅ Is this code safe from memory leaks (e.g., unbounded arrays/maps)?