The http Module
The http module is Node.jsβs built-in HTTP server and client. Without any frameworks like Express or Koa, it provides everything you need to handle HTTP requests and responses.
const http = require('http');
Why learn raw
http? Understanding the underlying HTTP module helps you debug framework issues, write middleware, and build custom servers when frameworks add too much overhead.
Creating an HTTP Server
The simplest possible HTTP server in Node.js:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
Letβs break down what happens here:
http.createServer()returns anet.Serverinstance (which extendsEventEmitter)- The callback is shorthand for
server.on('request', handler) reqis anhttp.IncomingMessage(a Readable stream)resis anhttp.ServerResponse(a Writable stream)server.listen()binds to the port and starts accepting connections
The req Object (IncomingMessage)
The request object contains everything about the incoming HTTP request:
const http = require('http');
const server = http.createServer((req, res) => {
// --- Request Line ---
console.log('Method:', req.method); // 'GET', 'POST', etc.
console.log('URL:', req.url); // '/api/users?page=1'
console.log('HTTP Version:', req.httpVersion); // '1.1'
// --- Headers ---
console.log('Headers:', req.headers);
// {
// 'content-type': 'application/json',
// 'user-agent': 'curl/8.0',
// 'accept': '*/*',
// 'host': 'localhost:3000'
// }
// --- URL Parsing (manual) ---
const url = new URL(req.url, `http://${req.headers.host}`);
console.log('Path:', url.pathname); // '/api/users'
console.log('Query:', url.searchParams.get('page')); // '1'
// --- Socket Info ---
console.log('Remote address:', req.socket.remoteAddress);
console.log('Remote port:', req.socket.remotePort);
res.end('OK');
});
Common req Properties
| Property | Type | Description |
|---|---|---|
req.method | string | HTTP method (GET, POST, PUT, DELETE, etc.) |
req.url | string | Request URL path + query string |
req.headers | object | HTTP headers (all lowercase keys) |
req.httpVersion | string | HTTP version (1.0, 1.1, 2.0) |
req.socket | net.Socket | Underlying TCP socket |
req.statusCode | number | Response status code (client-side only) |
req.statusMessage | string | Response status message (client-side only) |
The res Object (ServerResponse)
The response object is used to send data back to the client:
const http = require('http');
const server = http.createServer((req, res) => {
// --- Set Status Code ---
res.statusCode = 200;
// --- Set Headers (3 ways) ---
// Way 1: Set individual header
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Powered-By', 'Node.js');
// Way 2: Set all at once (overwrites previous headers)
res.writeHead(200, {
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
});
// Way 3: Convenience methods
res.getHeader('Content-Type'); // Read header
res.hasHeader('Content-Type'); // Check header exists
res.removeHeader('X-Custom-Header'); // Remove header
res.headersSent; // Boolean β have headers been sent?
// --- Send Body ---
// write() sends chunks, end() signals completion
res.write('<html>');
res.write('<body>');
res.write('<h1>Hello</h1>');
res.write('</body>');
res.write('</html>');
res.end(); // Must call end() to finish
// Shorthand: res.end(body)
// res.end(JSON.stringify({ message: 'Hello' }));
});
Critical: You must call
res.end()eventually. If you donβt, the client hangs until the timeout. Alwaysend()the response.
Response Flow
res.statusCode = 404
res.setHeader('Content-Type', 'text/plain')
res.write('Not found') β Can call multiple times for streaming
res.end() β Signals completion
Once res.end() is called:
- The response is fully sent to the client
- No more writes are allowed
- The
'finish'event fires on the response
Reading Request Body
The request (req) is a Readable stream. You need to collect the data manually:
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
// Collect data chunks
req.on('data', (chunk) => {
body += chunk.toString();
// β οΈ Warning: For large payloads, buffer to an array
// or use a size limit to prevent memory exhaustion
});
// All data received
req.on('end', () => {
console.log('Received body:', body);
try {
const data = JSON.parse(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: data }));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
} else {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method not allowed');
}
});
Buffer Body with Size Limit
function readBody(req, maxSize = 1024 * 1024) {
return new Promise((resolve, reject) => {
const chunks = [];
let totalSize = 0;
req.on('data', (chunk) => {
totalSize += chunk.length;
if (totalSize > maxSize) {
reject(new Error('Request body too large'));
req.destroy(); // Close the connection
return;
}
chunks.push(chunk);
});
req.on('end', () => {
resolve(Buffer.concat(chunks).toString());
});
req.on('error', reject);
});
}
// Usage
const body = await readBody(req, 512 * 1024); // Max 512KB
const data = JSON.parse(body);
Building a Simple Router
Without Express, you build routing manually:
const http = require('http');
const url = require('url');
// Route definitions
const routes = {
'GET /users': listUsers,
'GET /users/:id': getUser,
'POST /users': createUser,
'DELETE /users/:id': deleteUser,
};
function matchRoute(method, pathname) {
// Exact match first
const key = `${method} ${pathname}`;
if (routes[key]) return { handler: routes[key], params: {} };
// Parametrized match
for (const [routeKey, handler] of Object.entries(routes)) {
const [routeMethod, routePath] = routeKey.split(' ');
if (routeMethod !== method) continue;
const routeParts = routePath.split('/');
const pathParts = pathname.split('/');
if (routeParts.length !== pathParts.length) continue;
const params = {};
let match = true;
for (let i = 0; i < routeParts.length; i++) {
if (routeParts[i].startsWith(':')) {
params[routeParts[i].slice(1)] = pathParts[i];
} else if (routeParts[i] !== pathParts[i]) {
match = false;
break;
}
}
if (match) return { handler, params };
}
return null;
}
const server = http.createServer(async (req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// Handle preflight
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const match = matchRoute(req.method, parsedUrl.pathname);
if (!match) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
return;
}
// Enhanced req with parsed data
req.params = match.params;
req.query = parsedUrl.searchParams;
// Enhanced res with json helper
res.json = (data, status = 200) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
};
try {
await match.handler(req, res);
} catch (err) {
console.error('Route error:', err);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
}
});
// Route handlers
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
async function listUsers(req, res) {
res.json(users);
}
async function getUser(req, res) {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.json({ error: 'User not found' }, 404);
}
res.json(user);
}
async function createUser(req, res) {
const body = await readBody(req);
const data = JSON.parse(body);
const user = { id: users.length + 1, name: data.name };
users.push(user);
res.json(user, 201);
}
async function deleteUser(req, res) {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.json({ error: 'User not found' }, 404);
}
users.splice(index, 1);
res.json({ success: true });
}
server.listen(3000);
console.log('API server at http://localhost:3000/');
Request Events
The request object emits several events you can listen to:
req.on('data', (chunk) => {
// Called one or more times with body chunks
});
req.on('end', () => {
// Called exactly once when body is fully received
});
req.on('error', (err) => {
// Called if the request stream has an error
// e.g., connection terminated prematurely
});
req.on('aborted', () => {
// Called if the client disconnects before end
// After this, calling res.end() is safe (no-op)
// But you should stop processing
});
Response Events
res.on('close', () => {
// Called when the underlying connection is closed
// Could be before or after end() β check finished
});
res.on('finish', () => {
// Called when res.end() has been called and data is flushed
// All headers and body have been sent
});
res.on('error', (err) => {
// Response stream error
});
Streaming Responses
For large responses, write in chunks instead of buffering everything:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/large-file') {
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename="data.zip"',
});
// Stream directly from disk to response β never buffers in RAM
const readStream = fs.createReadStream('./large-file.zip');
readStream.pipe(res);
readStream.on('error', (err) => {
console.error('Stream error:', err);
if (!res.headersSent) {
res.writeHead(500);
}
res.end();
});
}
});
Streaming advantage: A 5GB file can be sent with < 64KB of RAM usage because data flows in chunks through the pipe.
Making HTTP Requests (Client-Side)
The http module also provides a client for making outgoing requests:
const http = require('http');
// GET request
http.get('http://api.example.com/users', (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const users = JSON.parse(data);
console.log('Users:', users);
});
}).on('error', (err) => {
console.error('Request failed:', err.message);
});
POST Request
const http = require('http');
const postData = JSON.stringify({ name: 'Charlie' });
const options = {
hostname: 'localhost',
port: 3000,
path: '/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
},
};
const req = http.request(options, (res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
console.log('Response:', body);
});
});
req.on('error', (err) => {
console.error('Request error:', err);
});
// Write body and send
req.write(postData);
req.end(); // Must call end() to send
Convenience: Using fetch (Node.js 18+)
Node.js now has a built-in fetch (based on undici):
// GET β no import needed, it's global
const response = await fetch('http://api.example.com/users');
const users = await response.json();
// POST
const createResponse = await fetch('http://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Charlie' }),
});
const newUser = await createResponse.json();
HTTPS Module
For TLS/SSL, use the https module instead:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem'),
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('Secure hello\n');
}).listen(443);
Key Takeaways
http.createServer(handler)creates a server; the handler receivesreqandresreqis anIncomingMessage(Readable stream) β hasmethod,url,headersresis aServerResponse(Writable stream) β usewriteHead(),write(),end()- Always call
res.end()β otherwise the client hangs forever - Read the body manually via
req.on('data')andreq.on('end')β itβs a stream - Set a body size limit when reading request bodies to prevent memory exhaustion
- Stream large responses with
.pipe()instead of buffering everything in RAM - Use
URLclass to parsereq.urlinto pathname and query parameters - Handle errors on both request and response streams β aborted connections are common
http.request()makes outgoing HTTP requests; Node.js 18+ also has built-infetchhttp.ServerextendsEventEmitterβ events:request,connection,close,error,upgrade- Raw http module is powerful but verbose β for complex apps, consider Express or Fastify on top