ADR-0006: Node.js 20 LTS as Runtime Environment
Status
Accepted - 2025-01-26
Context
TVL Platform requires a JavaScript runtime environment for backend services, background workers, and tooling with the following requirements:
Business Requirements
- Fast development velocity (< 3 months for MVP.0)
- Low operational overhead (small team)
- Cost-effective for MVP and scale
- Easy to hire developers
Technical Requirements
- Execute TypeScript/JavaScript code server-side
- High performance for API server (target: <200ms p95)
- Support for concurrent background workers
- Rich ecosystem of libraries (database, Redis, HTTP, etc.)
- Long-term support (LTS) for stability
Constraints
- Frontend uses React (JavaScript ecosystem)
- Team has JavaScript/TypeScript experience
- Must run on Docker (containerized deployment)
- PostgreSQL and Redis clients needed
Decision
Node.js 20 LTS (Long-Term Support) as the runtime environment for all backend code.
Version Pinning
// package.json
{
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=8.0.0"
  }
}
# Dockerfile
FROM node:20-alpine
20
Rationale
- LTS Stability: Node.js 20 LTS supported until April 2026 (18 months runway)
- Performance: V8 engine performance improvements (20% faster than v18)
- Modern Features: Native test runner, fetch API, Web Streams
- Ecosystem: 2.1 million npm packages available
- Team Expertise: Team already proficient in JavaScript/TypeScript
Alternatives Considered
Alternative 1: Bun
Rejected (for now)
Pros:
- 3x faster than Node.js (startup and runtime)
- Built-in TypeScript support (no compilation needed)
- Compatible with Node.js APIs
- Built-in bundler and test runner
Cons:
- Not yet production-ready (v1.0 released Oct 2023, still maturing)
- Smaller ecosystem (some npm packages incompatible)
- Less mature error handling and debugging
- Riskier for production workloads
Decision: Revisit Bun in 12-18 months when ecosystem matures.
Alternative 2: Deno
Rejected
Pros:
- Secure by default (permissions model)
- Native TypeScript support
- Modern standard library
- Better module system (ESM-first)
Cons:
- Incompatible with npm packages (requires import maps)
- Would need to rewrite npm dependencies
- Smaller ecosystem
- Less hiring pool (fewer Deno developers)
Decision: Node.js ecosystem too valuable to abandon.
Alternative 3: Node.js 18 LTS
Rejected
Pros:
- More mature (released April 2022)
- Longer track record
Cons:
- Node.js 20 LTS already stable (released April 2023)
- Missing performance improvements from v20
- No significant risk using v20 (LTS = stable)
Decision: Node.js 20 offers better performance with same stability.
Alternative 4: Python (FastAPI/Django)
Rejected
Pros:
- Excellent for data science and ML
- Great async support (asyncio)
- Easy to learn
Cons:
- Cannot share code with frontend (different language)
- Slower for I/O-bound workloads (GIL limits concurrency)
- Less suitable for real-time systems
- Team lacks Python expertise
Decision: Node.js better for full-stack JavaScript.
Consequences
Positive
- 
Performance - V8 JIT compiler optimizes hot paths
- Event loop handles 10,000+ concurrent connections
- Streaming I/O for large payloads
- Performance: <50ms API response times achievable
 
- 
Ecosystem - 2.1 million npm packages
- Excellent libraries: Fastify, Drizzle, BullMQ, Zod
- Active community and support
 
- 
Developer Experience - Single language for frontend + backend (code sharing)
- Excellent debugging (Chrome DevTools, VS Code debugger)
- Fast iteration (no compilation for JS, incremental for TS)
 
- 
Deployment - Small Docker images (node:20-alpine is 40MB)
- Fast cold starts (<1 second)
- Easy to containerize
 
- 
Hiring - Large talent pool (Node.js is #6 most used technology - Stack Overflow 2024)
- Easy to train frontend developers for backend
 
Negative
- 
Single-Threaded Event Loop - CPU-intensive tasks block event loop
- Mitigation: Use worker threads for CPU-bound work, or offload to background workers
 
- 
Memory Management - V8 garbage collection can cause latency spikes
- Mitigation: Tune GC flags (--max-old-space-size), use memory profiling
 
- 
Callback Hell / Async Complexity - Async code can be difficult to reason about
- Mitigation: Use async/await (native in Node.js 20), avoid callbacks
 
- 
NPM Dependency Hell - Dependency conflicts and security vulnerabilities
- Mitigation: Use pnpm (strict dependencies), Dependabot (security alerts), lock files
 
Node.js 20 Features Used
1. Native Test Runner
// No need for Jest/Vitest for simple tests
import { test, describe } from 'node:test';
import assert from 'node:assert';
describe('calculateTotal', () => {
  test('should add price and tax', () => {
    assert.strictEqual(calculateTotal(100, 0.1), 110);
  });
});
2. Native Fetch API
// No need for axios or node-fetch
const response = await fetch('https://api.hostaway.com/listings', {
  headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
3. Web Streams API
// Stream large CSV exports
const stream = fs.createReadStream('bookings.csv');
const response = new Response(stream);
4. Performance Hooks
import { PerformanceObserver } from 'node:perf_hooks';
const obs = new PerformanceObserver((items) => {
  console.log(items.getEntries()[0].duration);
});
obs.observe({ entryTypes: ['measure'] });
Implementation Guidelines
1. Pin Node.js Version
All environments (dev, staging, production) must use Node.js 20:
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
COPY . .
CMD ["node", "dist/server.js"]
2. Use Native APIs Where Possible
Prefer Node.js built-ins over npm packages:
// ✅ Good - native
import { test } from 'node:test';
await fetch('https://api.example.com');
// ❌ Avoid - unnecessary dependency
import axios from 'axios';
3. Configure V8 Heap Size
For production workloads:
# Increase heap size to 2GB (default is 512MB)
node --max-old-space-size=2048 dist/server.js
4. Enable Source Maps
For better error stack traces:
// tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true
  }
}
# Run with source map support
node --enable-source-maps dist/server.js
Migration Path
When to Upgrade
- Node.js 22 LTS (April 2025): Evaluate migration in Q3 2025
- Triggers: Security vulnerabilities, performance improvements, critical features
Upgrade Process
- Test in development environment
- Update Docker base image (FROM node:22-alpine)
- Update .nvmrcandpackage.jsonengines
- Run full test suite
- Deploy to staging for 1 week
- Deploy to production with canary rollout
Validation Checklist
-  All Dockerfiles use node:20-alpine
-  .nvmrcfile pinned to 20
-  package.jsonengines field requires Node.js 20+
- CI/CD uses Node.js 20
- Development team has Node.js 20 installed (via nvm)
- Source maps enabled for production debugging
- V8 heap size configured for production workloads