Every web framework you will ever use in Node.js — Express, Fastify, Next.js's server layer — is built on top of the http module. It is the core module that gives Node.js the ability to act as a web server: to listen for incoming requests, read what the client sent, and send a response back.
Understanding http directly is what makes everything Express does later feel obvious rather than magical. Express does not invent a new way of handling HTTP — it wraps http in a far more convenient API.
What HTTP Actually Is, Briefly
HTTP (Hypertext Transfer Protocol) is the protocol that defines how a client — a browser, a mobile app, another server — communicates with a server over the network. A client sends a request: a method (GET, POST, PUT, DELETE, and others), a URL, headers, and sometimes a body of data. The server processes that request and sends back a response: a status code, headers, and usually a body.
Node.js's http module gives you direct, low-level control over both halves of this exchange.
Creating a Basic Server
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, Wariz');
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
http.createServer() takes a single function — the request handler — and returns a server object. This function runs every single time a request reaches the server. It receives two objects: req (the incoming request) and res (the response you build and send back).
server.listen(3000, callback) starts the server, binding it to port 3000 on the local machine. The callback runs once the server is actually listening and ready to accept connections.
Run this file with node server.js, then visiting http://localhost:3000 in a browser, or running curl http://localhost:3000 in a terminal, returns Hello, Wariz.
The Request Object — req
The req object represents the incoming HTTP request and carries everything the client sent.
const server = http.createServer((req, res) => {
console.log(req.method); // GET, POST, PUT, DELETE, etc.
console.log(req.url); // /users/42?active=true
console.log(req.headers); // { host: 'localhost:3000', 'user-agent': '...', ... }
res.end('Received');
});
| Property | What It Contains |
|---|---|
req.method | The HTTP method used — GET, POST, PUT, DELETE, PATCH, etc. |
req.url | The path and query string of the request, e.g. /users/42?active=true |
req.headers | An object containing every HTTP header sent by the client |
Parsing the URL
req.url is a raw string — it includes the path and the query string together, unparsed. Node.js's built-in url module is typically used alongside http to break this apart cleanly:
const http = require('http');
const { URL } = require('url');
const server = http.createServer((req, res) => {
const fullUrl = new URL(req.url, `http://${req.headers.host}`);
console.log(fullUrl.pathname); // /users
console.log(fullUrl.searchParams.get('active')); // 'true'
res.end('Parsed');
});
The URL constructor requires a base URL as its second argument because req.url alone is only a path, not a complete URL — the base is reconstructed from the host header.
Reading the Request Body
For methods like POST and PUT, the client typically sends data in the request body. Unlike req.url and req.headers, the body is not immediately available as a property — it arrives as a stream, in chunks, and must be collected manually:
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
console.log('Body received:', body);
const parsed = JSON.parse(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: parsed }));
});
}
});
req is a readable stream. The 'data' event fires each time a chunk of the body arrives; the 'end' event fires once the entire body has been received. This manual chunk-collection is exactly the kind of verbosity Express eliminates — in Express, the request body is available as a parsed object directly, with a single line of middleware setup. Streams are covered in full in their own dedicated topic later in this series.
The Response Object — res
The res object is how you build and send the response back to the client.
Setting the Status Code and Headers
res.writeHead(200, { 'Content-Type': 'text/html' });
res.writeHead(statusCode, headers) sets the HTTP status code and any response headers in a single call. The status code must be set before any part of the response body is sent — once you start writing the body, headers can no longer be changed.
Common status codes:
| Code | Meaning |
|---|---|
200 | OK — the request succeeded |
201 | Created — a new resource was successfully created |
400 | Bad Request — the client sent invalid data |
401 | Unauthorized — authentication is required |
403 | Forbidden — the client is authenticated but not permitted |
404 | Not Found — the requested resource does not exist |
500 | Internal Server Error — something failed on the server |
You can also set the status code and headers separately:
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
Sending the Response Body
res.write('Part of the response... ');
res.write('and more...');
res.end('Final part — this also ends the response');
res.write() sends a chunk of the response body without closing the connection — more can be written afterward. res.end() sends the final chunk (optional) and signals that the response is complete. Every response must call res.end() — without it, the connection remains open and the client waits indefinitely.
For most simple responses, a single res.end(data) call is sufficient — there is no need to use res.write() unless you are streaming a response incrementally.
Sending JSON
const server = http.createServer((req, res) => {
const data = { message: 'Hello, Wariz', timestamp: Date.now() };
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
});
JSON.stringify() converts the JavaScript object to a string — res.end() can only send strings or buffers, never a raw object.
Routing — Handling Different URLs and Methods
The http module has no built-in routing. Every request — regardless of URL or method — passes through the same single handler function. Routing, if you want it, must be built manually by inspecting req.method and req.url:
const http = require('http');
const { URL } = require('url');
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Home page');
return;
}
if (req.method === 'GET' && url.pathname === '/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ id: 1, name: 'Wariz' }]));
return;
}
if (req.method === 'POST' && url.pathname === '/users') {
// handle creating a user
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ created: true }));
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});
This works, but it is immediately clear why frameworks exist. A growing application with dozens of routes, each needing its own logic, middleware, and parameter parsing, becomes unmanageable as a chain of if statements. Express was built specifically to replace this pattern with a declarative routing API — app.get('/users', handler) — that does the same job in a fraction of the code.
Making HTTP Requests as a Client
http is not only for building servers — it can also make outgoing HTTP requests, acting as a client.
const http = require('http');
http.get('http://api.example.com/data', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('Response:', JSON.parse(data));
});
});
This is the low-level mechanism behind every npm package that fetches data from an external API. In modern Node.js (version 18 and later), the global fetch() function is available natively and is generally the more convenient choice for making outgoing requests — it returns a Promise and works the same way it does in the browser:
async function getData() {
const response = await fetch('http://api.example.com/data');
const data = await response.json();
console.log(data);
}
http.get() and http.request() remain useful when you need lower-level control — custom agents, connection pooling, or streaming a response — but for typical use cases, fetch() is now the simpler and more commonly used option.
https — The Secure Variant
For HTTPS servers and requests, Node.js provides a near-identical https module:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem'),
};
const server = https.createServer(options, (req, res) => {
res.end('Secure connection');
});
server.listen(443);
The API mirrors http almost exactly — the difference is the additional options object containing the TLS certificate and private key needed to encrypt the connection. In practice, most production Node.js applications do not terminate TLS themselves — that responsibility is typically handled by a reverse proxy or hosting platform (Vercel, Nginx, a load balancer) in front of the application, with the Node.js server itself running plain HTTP behind it.
Why This Module Matters Even Though You Will Rarely Use It Directly
In real projects, you are very unlikely to build a server directly on the raw http module — Express or another framework will be the actual interface you work with. The value of this topic is not that you will write code like this often. It is that everything Express provides — req.body, req.params, res.json(), route handlers — is a convenience layer sitting directly on top of the req and res objects covered here.
When something behaves unexpectedly in Express — a header is not set correctly, a response hangs, a body fails to parse — understanding what is actually happening underneath at the http module level is often what makes the problem diagnosable.
Summary
| Task | Method |
|---|---|
| Create a server | http.createServer((req, res) => {...}) |
| Start listening | server.listen(port, callback) |
| Get the request method | req.method |
| Get the request URL | req.url |
| Get request headers | req.headers |
| Read the request body | req.on('data', ...) and req.on('end', ...) |
| Set the status code and headers | res.writeHead(statusCode, headers) |
| Send response data | res.write(data) and res.end(data) |
| Make an outgoing request | http.get(url, callback) or the global fetch() |
| Create a secure server | https.createServer(options, handler) |