Node.js Best Practices
Principles and decision-making for Node.js development in 2025.
Learn to THINK, not memorize code patterns.
ā ļø How to Use This Skill
This skill teaches decision-making principles, not fixed code to copy.
- ASK user for preferences when unclear
- Choose framework/pattern based on CONTEXT
- Don't default to same solution every time
1. Framework Selection (2025)
Decision Tree
What are you building?
ā
āāā Edge/Serverless (Cloudflare, Vercel)
ā āāā Hono (zero-dependency, ultra-fast cold starts)
ā
āāā High Performance API
ā āāā Fastify (2-3x faster than Express)
ā
āāā Enterprise/Team familiarity
ā āāā NestJS (structured, DI, decorators)
ā
āāā Legacy/Stable/Maximum ecosystem
ā āāā Express (mature, most middleware)
ā
āāā Full-stack with frontend
āāā Next.js API Routes or tRPC
Comparison Principles
| Factor |
Hono |
Fastify |
Express |
| Best for |
Edge, serverless |
Performance |
Legacy, learning |
| Cold start |
Fastest |
Fast |
Moderate |
| Ecosystem |
Growing |
Good |
Largest |
| TypeScript |
Native |
Excellent |
Good |
| Learning curve |
Low |
Medium |
Low |
Selection Questions to Ask:
- What's the deployment target?
- Is cold start time critical?
- Does team have existing experience?
- Is there legacy code to maintain?
2. Runtime Considerations (2025)
Native TypeScript
Node.js 22+: --experimental-strip-types
āāā Run .ts files directly
āāā No build step needed for simple projects
āāā Consider for: scripts, simple APIs
Module System Decision
ESM (import/export)
āāā Modern standard
āāā Better tree-shaking
āāā Async module loading
āāā Use for: new projects
CommonJS (require)
āāā Legacy compatibility
āāā More npm packages support
āāā Use for: existing codebases, some edge cases
Runtime Selection
| Runtime |
Best For |
| Node.js |
General purpose, largest ecosystem |
| Bun |
Performance, built-in bundler |
| Deno |
Security-first, built-in TypeScript |
3. Architecture Principles
Layered Structure Concept
Request Flow:
ā
āāā Controller/Route Layer
ā āāā Handles HTTP specifics
ā āāā Input validation at boundary
ā āāā Calls service layer
ā
āāā Service Layer
ā āāā Business logic
ā āāā Framework-agnostic
ā āāā Calls repository layer
ā
āāā Repository Layer
āāā Data access only
āāā Database queries
āāā ORM interactions
Why This Matters:
- Testability: Mock layers independently
- Flexibility: Swap database without touching business logic
- Clarity: Each layer has single responsibility
When to Simplify:
- Small scripts ā Single file OK
- Prototypes ā Less structure acceptable
- Always ask: "Will this grow?"
4. Error Handling Principles
Centralized Error Handling
Pattern:
āāā Create custom error classes
āāā Throw from any layer
āāā Catch at top level (middleware)
āāā Format consistent response
Error Response Philosophy
Client gets:
āāā Appropriate HTTP status
āāā Error code for programmatic handling
āāā User-friendly message
āāā NO internal details (security!)
Logs get:
āāā Full stack trace
āāā Request context
āāā User ID (if applicable)
āāā Timestamp
Status Code Selection
| Situation |
Status |
When |
| Bad input |
400 |
Client sent invalid data |
| No auth |
401 |
Missing or invalid credentials |
| No permission |
403 |
Valid auth, but not allowed |
| Not found |
404 |
Resource doesn't exist |
| Conflict |
409 |
Duplicate or state conflict |
| Validation |
422 |
Schema valid but business rules fail |
| Server error |
500 |
Our fault, log everything |
5. Async Patterns Principles
When to Use Each
| Pattern |
Use When |
async/await |
Sequential async operations |
Promise.all |
Parallel independent operations |
Promise.allSettled |
Parallel where some can fail |
Promise.race |
Timeout or first response wins |
Event Loop Awareness
I/O-bound (async helps):
āāā Database queries
āāā HTTP requests
āāā File system
āāā Network operations
CPU-bound (async doesn't help):
āāā Crypto operations
āāā Image processing
āāā Complex calculations
āāā ā Use worker threads or offload
Avoiding Event Loop Blocking
- Never use sync methods in production (fs.readFileSync, etc.)
- Offload CPU-intensive work
- Use streaming for large data
6. Validation Principles
Validate at Boundaries
Where to validate:
āāā API entry point (request body/params)
āāā Before database operations
āāā External data (API responses, file uploads)
āāā Environment variables (startup)
Validation Library Selection
| Library |
Best For |
| Zod |
TypeScript first, inference |
| Valibot |
Smaller bundle (tree-shakeable) |
| ArkType |
Performance critical |
| Yup |
Existing React Form usage |
Validation Philosophy
- Fail fast: Validate early
- Be specific: Clear error messages
- Don't trust: Even "internal" data
7. Security Principles
Security Checklist (Not Code)
Security Mindset
Trust nothing:
āāā Query params ā validate
āāā Request body ā validate
āāā Headers ā verify
āāā Cookies ā validate
āāā File uploads ā scan
āāā External APIs ā validate response
8. Testing Principles
Test Strategy Selection
| Type |
Purpose |
Tools |
| Unit |
Business logic |
node:test, Vitest |
| Integration |
API endpoints |
Supertest |
| E2E |
Full flows |
Playwright |
What to Test (Priorities)
- Critical paths: Auth, payments, core business
- Edge cases: Empty inputs, boundaries
- Error handling: What happens when things fail?
- Not worth testing: Framework code, trivial getters
Built-in Test Runner (Node.js 22+)
node --test src/**/*.test.ts
āāā No external dependency
āāā Good coverage reporting
āāā Watch mode available
10. Anti-Patterns to Avoid
ā DON'T:
- Use Express for new edge projects (use Hono)
- Use sync methods in production code
- Put business logic in controllers
- Skip input validation
- Hardcode secrets
- Trust external data without validation
- Block event loop with CPU work
ā
DO:
- Choose framework based on context
- Ask user for preferences when unclear
- Use layered architecture for growing projects
- Validate all inputs
- Use environment variables for secrets
- Profile before optimizing
11. Decision Checklist
Before implementing:
Remember: Node.js best practices are about decision-making, not memorizing patterns. Every project deserves fresh consideration based on its requirements.