xecutes whenever a client establishes a connection.
import http from 'http';
const PORT = 3000;
const requestHandler = (
incoming: http.IncomingMessage,
outgoing: http.ServerResponse
): void => {
outgoing.writeHead(200, { 'Content-Type': 'text/plain' });
outgoing.end('Service operational');
};
const networkListener = http.createServer(requestHandler);
networkListener.listen(PORT, () => {
console.log(`Endpoint active on port ${PORT}`);
});
Architecture Rationale: We separate the handler definition from the server instantiation. This pattern improves testability and allows the handler to be reused across different server configurations (e.g., HTTPS, HTTP/2). The listen() callback is asynchronous; Node.js invokes it only after the OS successfully binds the port, preventing race conditions during startup.
Step 2: Implement Path-Based Routing
Raw HTTP requests arrive with a url property containing the full request path. We can route traffic by inspecting this value before generating a response.
import http from 'http';
const routeDispatcher = (
incoming: http.IncomingMessage,
outgoing: http.ServerResponse
): void => {
const targetPath = incoming.url || '/';
if (targetPath === '/') {
outgoing.writeHead(200, { 'Content-Type': 'text/html' });
outgoing.end('<h1>Root Endpoint</h1>');
} else if (targetPath === '/status') {
outgoing.writeHead(200, { 'Content-Type': 'application/json' });
outgoing.end(JSON.stringify({ uptime: process.uptime(), state: 'healthy' }));
} else {
outgoing.writeHead(404, { 'Content-Type': 'text/plain' });
outgoing.end('Route undefined');
}
};
const service = http.createServer(routeDispatcher);
service.listen(3000);
Architecture Rationale: Conditional routing avoids framework overhead. We explicitly set Content-Type headers per route to prevent browser/client misinterpretation. The 404 fallback ensures unhandled paths return a proper status code instead of hanging the connection.
Step 3: Parse Query Parameters
Real-world applications require extracting dynamic values from URLs. Node.js provides the native url module for structured parsing. Passing true as the second argument automatically converts query strings into JavaScript objects.
import http from 'http';
import url from 'url';
const queryExtractor = (
incoming: http.IncomingMessage,
outgoing: http.ServerResponse
): void => {
const parsed = url.parse(incoming.url || '', true);
const parameters = parsed.query;
outgoing.writeHead(200, { 'Content-Type': 'text/html' });
outgoing.write('<h2>Extracted Parameters</h2><ul>');
for (const key in parameters) {
if (Object.prototype.hasOwnProperty.call(parameters, key)) {
const value = Array.isArray(parameters[key])
? parameters[key].join(', ')
: parameters[key];
outgoing.write(`<li>${key}: ${value}</li>`);
}
}
outgoing.end('</ul>');
};
const parameterServer = http.createServer(queryExtractor);
parameterServer.listen(3000);
Architecture Rationale: The url.parse() method safely decodes URL-encoded characters. We check hasOwnProperty to avoid prototype pollution when iterating. Array handling accounts for repeated query keys (e.g., ?tag=js&tag=node), which the parser converts to arrays. This approach eliminates regex-based parsing and reduces vulnerability to malformed inputs.
Step 4: Integrate Method Awareness
HTTP services must differentiate between GET, POST, PUT, and DELETE operations. The incoming.method property provides this context.
const methodAwareHandler = (
incoming: http.IncomingMessage,
outgoing: http.ServerResponse
): void => {
const method = incoming.method || 'GET';
const path = incoming.url || '/';
if (method === 'GET' && path === '/data') {
outgoing.writeHead(200, { 'Content-Type': 'application/json' });
outgoing.end(JSON.stringify({ source: 'native', version: 1 }));
} else if (method === 'POST' && path === '/data') {
outgoing.writeHead(201, { 'Content-Type': 'text/plain' });
outgoing.end('Payload accepted');
} else {
outgoing.writeHead(405, { 'Content-Type': 'text/plain' });
outgoing.end('Method not permitted');
}
};
Architecture Rationale: Explicit method checking prevents unintended data mutations. Returning 405 Method Not Allowed for unsupported operations aligns with RFC 7231 standards and improves API contract clarity.
Pitfall Guide
1. Omitting Response Termination
Explanation: Failing to call outgoing.end() leaves the TCP connection open indefinitely. Clients will timeout, and server resources will leak.
Fix: Always terminate every code path with end(), even for empty responses or error states.
2. Blocking the Event Loop
Explanation: Synchronous operations (fs.readFileSync, heavy CPU loops) freeze the single-threaded Event Loop, halting all concurrent requests.
Fix: Use asynchronous APIs (fs.promises, stream pipelines) or offload CPU-intensive work to Worker Threads.
3. Ignoring HTTP Method Constraints
Explanation: Treating all requests as GET allows clients to trigger destructive operations via unsafe methods.
Fix: Validate incoming.method before executing business logic. Return 405 for unsupported verbs.
4. Mishandling URL-Encoded Characters
Explanation: Raw request.url strings contain percent-encoded sequences (%20, %2B). Direct string comparison fails.
Fix: Always use url.parse() or the modern URL class to decode paths and query parameters safely.
5. Hardcoding Status Codes
Explanation: Magic numbers (200, 404, 500) reduce readability and increase typo risk.
Fix: Reference http.STATUS_CODES or maintain a centralized enum for status mappings.
Explanation: Sending HTML without Content-Type: text/html causes browsers to render raw markup. Missing Cache-Control headers trigger unnecessary revalidation.
Fix: Explicitly set headers per response. Use res.setHeader() for granular control.
7. Treating request.url as Static
Explanation: The url property includes query strings and fragments. Direct equality checks (req.url === '/api') fail when parameters are appended.
Fix: Extract the pathname using url.parse(req.url).pathname before routing.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Serverless / Cold-Start Sensitive | Native http Module | Minimal initialization latency and memory footprint | Low compute cost, faster scaling |
| Complex Middleware Chains | Express/Fastify | Built-in body parsing, CORS, and routing abstractions | Higher memory usage, moderate latency |
| High-Throughput API Gateway | Native http + Stream Pipelines | Direct socket control, zero abstraction overhead | Low infrastructure cost, requires engineering time |
| Legacy Migration / Rapid Prototyping | Framework-based | Accelerated development with community plugins | Higher long-term maintenance cost |
Configuration Template
import http from 'http';
import url from 'url';
interface RouteConfig {
method: string;
path: string;
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void;
}
const ROUTES: RouteConfig[] = [
{
method: 'GET',
path: '/',
handler: (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Native HTTP Service</h1>');
}
},
{
method: 'GET',
path: '/metrics',
handler: (req, res) => {
const parsed = url.parse(req.url || '', true);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
uptime: process.uptime(),
memory: process.memoryUsage().rss,
query: parsed.query
}));
}
}
];
const router = (req: http.IncomingMessage, res: http.ServerResponse): void => {
const pathname = url.parse(req.url || '').pathname || '/';
const method = req.method || 'GET';
const match = ROUTES.find(r => r.method === method && r.path === pathname);
if (match) {
match.handler(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Endpoint not registered');
}
};
const server = http.createServer(router);
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Service bound to port ${PORT}`);
});
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
Quick Start Guide
- Initialize Project: Run
npm init -y in your target directory.
- Add TypeScript Support: Execute
npm install typescript @types/node --save-dev and run npx tsc --init.
- Create Entry File: Save the configuration template as
server.ts.
- Compile & Execute: Run
npx tsc server.ts followed by node server.js.
- Verify: Open
http://localhost:3000/ in your browser or use curl http://localhost:3000/metrics?debug=true to test routing and query parsing.