06 - HTTP from Scratch
📋 Jump to TakeawaysThis lesson uses ES Modules. Make sure your package.json has
"type": "module". See Modules if you need a refresher.
A Server in 6 Lines
Node.js has a built-in HTTP module. No Express, no Fastify, no frameworks.
import http from 'node:http';
const server = http.createServer((req, res) => {
res.end('Hello from Node.js');
});
server.listen(3000, () => console.log('Running on http://localhost:3000'));That's a working web server. Every request hits the callback. req is what came in, res is what you send back.
Request Object
The req object tells you what the client asked for.
const server = http.createServer((req, res) => {
console.log(req.method); // "GET", "POST", etc.
console.log(req.url); // "/users", "/api/data"
console.log(req.headers); // { host: 'localhost:3000', ... }
res.end('OK');
});Response Object
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Hello' }));
});res.end() finishes the response. You must call it or the request hangs forever.
Simple Router
No framework needed for basic routing. Just check req.method and req.url.
import http from 'node:http';
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.method === 'GET' && req.url === '/') {
res.end(JSON.stringify({ status: 'ok' }));
} else if (req.method === 'GET' && req.url === '/users') {
res.end(JSON.stringify([{ id: 1, name: 'Alice' }]));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not found' }));
}
});
server.listen(3000, () => console.log('Running on http://localhost:3000'));This is exactly what Express does under the hood, just with nicer syntax.
Reading Request Body
Request bodies come in chunks. You need to collect them.
import http from 'node:http';
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/users') {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
const user = JSON.parse(body);
console.log('Created user:', user);
res.statusCode = 201;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ created: user }));
});
} else {
res.statusCode = 404;
res.end('Not found');
}
});
server.listen(3000, () => console.log('Running on http://localhost:3000'));The body arrives as a stream. data events give you chunks, end fires when it's complete.
Test it with curl:
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"Alice"}'
# {"created":{"name":"Alice"}}Serving Static Files
First, create a public folder with an index.html:
mkdir public<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><title>My Server</title></head>
<body>
<h1>It works!</h1>
<p>Served by Node.js with zero frameworks.</p>
</body>
</html>Now serve it:
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
const MIME = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
};
const server = http.createServer((req, res) => {
const filePath = path.join(import.meta.dirname, 'public', req.url === '/' ? 'index.html' : req.url);
const ext = path.extname(filePath);
fs.readFile(filePath, (err, data) => {
if (err) {
res.statusCode = 404;
res.end('Not found');
return;
}
res.setHeader('Content-Type', MIME[ext] || 'text/plain');
res.end(data);
});
});
server.listen(3000, () => console.log('Serving public/ on http://localhost:3000'));This is what npx serve does. Read the file, set the content type, send it back. Note we use import.meta.dirname instead of __dirname in ESM.
Making HTTP Requests
Node 18+ has the global fetch, same as browsers.
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todo = await res.json();
console.log(todo);
// { userId: 1, id: 1, title: '...', completed: false }No need for axios or node-fetch anymore. Built-in fetch works.
Key Takeaways
http.createServergives you a server with zero dependencies- Every request gets
req(what came in) andres(what you send back) - Always call
res.end()or the request hangs - Route by checking
req.methodandreq.url, that's all frameworks do - Request bodies arrive as streams, collect chunks with
dataandendevents - In ESM, use
import.meta.dirnameinstead of__dirname - Node 18+ has built-in
fetchfor making HTTP requests, no extra packages needed