versions:
node --version
npm --version
If the shell returns command not found, the executable is not in your system PATH. This typically occurs when the installer was run without administrative privileges or when terminal sessions were not refreshed. Restart the terminal or reboot the host to propagate environment variables.
Step 2: Runtime Exploration via REPL
The Read-Eval-Print Loop (REPL) provides an interactive sandbox for testing runtime behavior without file I/O overhead. Launch it by executing node with no arguments. The REPL evaluates expressions synchronously and maintains a persistent context until termination.
Key runtime inspection commands:
> process.version
'v20.18.1'
> process.platform
'darwin'
> process.arch
'arm64'
> process.memoryUsage()
{ rss: 45219840, heapTotal: 15728640, heapUsed: 8432100, external: 2145000, arrayBuffers: 102400 }
The REPL is ideal for validating module imports, testing regex patterns, or inspecting process metrics. Exit using .exit, Ctrl + D, or double Ctrl + C. Note that __dirname and __filename are undefined in the REPL because they are file-scoped variables injected by the module loader, not global constants.
Step 3: Project Scaffolding & Module Architecture
Initialize a dedicated directory and establish a clean module boundary. We will use TypeScript to enforce type safety across request/response interfaces, then compile to CommonJS for maximum compatibility with legacy tooling.
mkdir node-runtime-gateway
cd node-runtime-gateway
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
Configure tsconfig.json to target modern JavaScript while preserving Node.js module resolution:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
We will construct a routing engine that maps URL paths to handler functions, manages response headers explicitly, and enforces stream termination. This approach eliminates framework overhead while maintaining production-grade reliability.
src/types.ts
import { IncomingMessage, ServerResponse } from 'http';
export interface RouteHandler {
(req: IncomingMessage, res: ServerResponse): void;
}
export interface RouteRegistry {
[path: string]: RouteHandler;
}
src/server.ts
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
import { RouteRegistry } from './types';
// Route registry: maps endpoints to handler functions
const routes: RouteRegistry = {
'/': (req: IncomingMessage, res: ServerResponse) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<body>
<h1>Runtime Gateway</h1>
<p>Node.js ${process.version} | Platform: ${process.platform}</p>
<nav>
<a href="/health">Health Check</a> |
<a href="/metrics">Metrics</a>
</nav>
</body>
</html>
`);
},
'/health': (_req: IncomingMessage, res: ServerResponse) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
},
'/metrics': (_req: IncomingMessage, res: ServerResponse) => {
const mem = process.memoryUsage();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
rss: Math.round(mem.rss / 1024 / 1024),
pid: process.pid
}));
}
};
// Request dispatcher
function requestHandler(req: IncomingMessage, res: ServerResponse): void {
const targetPath = req.url?.split('?')[0] || '/';
const handler = routes[targetPath];
if (handler) {
handler(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404: Route not registered');
}
}
// Server initialization
const PORT: number = parseInt(process.env.PORT || '3000', 10);
const HOST: string = process.env.HOST || '127.0.0.1';
const server: Server = createServer(requestHandler);
server.listen(PORT, HOST, () => {
console.log(`[Gateway] Listening on http://${HOST}:${PORT}`);
console.log(`[Runtime] Node.js ${process.version} | ${process.platform}`);
});
// Graceful shutdown handler
function shutdown(signal: string): void {
console.log(`\n[Gateway] Received ${signal}. Initiating graceful shutdown...`);
server.close(() => {
console.log('[Gateway] All connections closed. Exiting.');
process.exit(0);
});
// Force exit after 10s if connections hang
setTimeout(() => {
console.error('[Gateway] Forced shutdown after timeout.');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Architecture Rationale
- Explicit Route Registry: Using a lookup object instead of
if/else chains reduces cyclomatic complexity and enables O(1) path resolution. This pattern scales cleanly when integrating with external routers later.
- Environment-Driven Configuration: Hardcoding ports and hosts breaks container orchestration. Reading from
process.env allows Docker, Kubernetes, and CI/CD pipelines to inject runtime values without code changes.
- Stream Termination Enforcement: Every response path calls
res.end(). Omitting this leaves TCP sockets open, exhausting file descriptors under load.
- Graceful Shutdown: Orchestration platforms send
SIGTERM before killing containers. Closing the server stops accepting new connections while allowing active requests to finish, preventing dropped transactions and client-side timeouts.
- TypeScript Strict Mode: Enforces correct
IncomingMessage and ServerResponse typing, catching header mismatches and missing status codes at compile time rather than runtime.
Pitfall Guide
1. Forgetting res.end() or res.write()
Explanation: The HTTP response stream remains open until explicitly terminated. Browsers will hang indefinitely, and the server will leak memory tracking pending sockets.
Fix: Always pair res.writeHead() with res.end(). For streaming responses, use res.write() followed by res.end() when the payload completes.
2. Blocking the Event Loop with Synchronous I/O
Explanation: Functions like fs.readFileSync() or heavy CPU computations freeze the main thread. Node.js is single-threaded; blocking it prevents all other requests from being processed.
Fix: Use asynchronous alternatives (fs.promises.readFile, crypto.scrypt) or offload CPU-heavy work to Worker Threads or child processes.
3. __dirname Undefined in ES Modules
Explanation: CommonJS injects __dirname and __filename automatically. ES Modules do not. Attempting to access them throws a ReferenceError.
Fix: In ESM, derive the directory using import.meta.url:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
4. Ignoring Stream Error Events
Explanation: HTTP requests and responses are streams. If a client disconnects mid-transfer, the server emits an error event. Unhandled stream errors crash the entire Node.js process.
Fix: Attach error listeners to req and res:
req.on('error', (err) => console.error('Request stream error:', err));
res.on('error', (err) => console.error('Response stream error:', err));
5. Assuming process.cwd() Equals Script Directory
Explanation: process.cwd() returns the directory where the node command was executed, not where the script resides. Running node dist/server.js from /home/user while the script lives in /opt/app breaks relative file paths.
Fix: Use __dirname (CJS) or import.meta.url (ESM) for file-relative paths. Reserve process.cwd() for CLI tools that intentionally operate in the user's working directory.
6. Hardcoding Ports and Hosts
Explanation: Binding to localhost or fixed ports fails in cloud environments where ports are dynamically assigned and hosts must listen on 0.0.0.0 to accept external traffic.
Fix: Read configuration from environment variables. Default to 0.0.0.0 for production deployments and 127.0.0.1 for local development.
7. Skipping Graceful Shutdown Logic
Explanation: Container orchestrators terminate processes abruptly if no shutdown handler exists. Active database transactions, file writes, and WebSocket connections are severed, causing data corruption and client errors.
Fix: Listen for SIGTERM/SIGINT, call server.close(), drain connection pools, and exit with code 0 on success or 1 on timeout.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Learning / Prototyping | Raw http module + REPL | Zero dependencies, immediate feedback, teaches runtime fundamentals | $0 |
| Small API / Internal Tool | Bare-metal server + route registry | Minimal overhead, fast startup, easy to containerize | Low (single instance) |
| Enterprise Microservice | Framework (Express/Fastify) + TypeScript | Built-in middleware, validation, OpenAPI generation, team standardization | Medium (framework tax) |
| High-Concurrency Gateway | Raw http + Cluster module / PM2 | Maximizes CPU utilization, avoids framework abstraction penalties | Medium-High (multi-core) |
| Containerized Deployment | Environment-driven config + graceful shutdown | Ensures Kubernetes/Docker compatibility, prevents dropped connections | Low (infrastructure standard) |
Configuration Template
package.json
{
"name": "node-runtime-gateway",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "ts-node src/server.ts"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.11.0"
}
}
.env.example
PORT=3000
HOST=0.0.0.0
NODE_ENV=production
LOG_LEVEL=info
Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
Quick Start Guide
- Initialize Project: Run
mkdir gateway && cd gateway && npm init -y && npm i typescript @types/node -D
- Configure TypeScript: Create
tsconfig.json with strict compilation and outDir: ./dist
- Write Server Code: Copy the
src/server.ts and src/types.ts implementations into your project
- Build & Run: Execute
npx tsc && PORT=8080 node dist/server.js
- Verify: Open
http://localhost:8080/health in your browser or run curl http://localhost:8080/metrics to confirm routing and runtime metrics are functioning correctly.