Basic HTTP Server

Creating a minimal server with Node.js built-in http module

Node.js includes a built-in http module that lets you create HTTP servers without any external dependencies. This is the foundation that frameworks like Express are built upon.

Understanding the raw HTTP module helps you understand what's happening under the hood and gives you more control when you need it.

Minimal Server Node.js

server.js
import { createServer } from 'node:http'; const server = createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('Hello, World!'); }); server.listen(3000, () => { console.log('Server running at http://localhost:3000/'); });

How it works:

  • createServer() takes a callback that runs for every request
  • request contains info about the incoming request (method, URL, headers)
  • response is used to send data back to the client
  • writeHead() sets the status code and headers
  • end() sends the body and signals the response is complete

Run it

$ node server.js
Server running at http://localhost:3000/

Accessing Request Information

server.js
import { createServer } from 'node:http'; const server = createServer((req, res) => { // Request metadata console.log(req.method); // "GET", "POST", etc. console.log(req.url); // "/users?id=123" console.log(req.headers.host); // "localhost:3000" console.log(req.headers['user-agent']); // Parse the URL const url = new URL(req.url, `http://${req.headers.host}`); console.log(url.pathname); // "/users" console.log(url.searchParams.get('id')); // "123" // Send response res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams) })); }); server.listen(3000);

The request object is a readable stream that also includes metadata like method, url, and headers. Use the URL constructor to parse the URL and extract path and query parameters.

Reading the Request Body

server.js
import { createServer } from 'node:http'; // Helper to read request body as JSON async function parseJSON(request) { const chunks = []; for await (const chunk of request) { chunks.push(chunk); } const body = Buffer.concat(chunks).toString(); return body ? JSON.parse(body) : null; } const server = createServer(async (req, res) => { if (req.method === 'POST') { try { const data = await parseJSON(req); console.log('Received:', data); res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, data })); } catch (error) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON' })); } } else { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Send a POST request with JSON body'); } }); server.listen(3000);

The request body arrives as a stream of chunks. Use for await...of to collect them, then concatenate and parse. Always handle parse errors gracefully.

Test with curl

$ curl -X POST http://localhost:3000 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

{"success":true,"data":{"name":"Alice","email":"alice@example.com"}}

Response Headers & Status Codes

server.js
const server = createServer((req, res) => { // Set individual headers res.setHeader('Content-Type', 'application/json'); res.setHeader('X-Request-ID', crypto.randomUUID()); res.setHeader('Cache-Control', 'no-store'); // CORS headers (for cross-origin requests) res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); // Set status code res.statusCode = 200; // Or use writeHead to set status and headers together // res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'Success' })); });

Use setHeader() for individual headers or writeHead() to set status and multiple headers at once. Headers must be set before calling write() or end().

Complete Example

server.js
import { createServer } from 'node:http'; const PORT = process.env.PORT || 3000; async function parseJSON(req) { const chunks = []; for await (const chunk of req) chunks.push(chunk); const body = Buffer.concat(chunks).toString(); return body ? JSON.parse(body) : null; } function sendJSON(res, status, data) { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } const server = createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const { method } = req; const { pathname } = url; console.log(`${method} ${pathname}`); // Health check if (pathname === '/health') { return sendJSON(res, 200, { status: 'ok' }); } // Echo endpoint if (pathname === '/echo' && method === 'POST') { try { const body = await parseJSON(req); return sendJSON(res, 200, { method, path: pathname, query: Object.fromEntries(url.searchParams), body }); } catch { return sendJSON(res, 400, { error: 'Invalid JSON' }); } } // 404 for everything else sendJSON(res, 404, { error: 'Not Found' }); }); server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}/`); });

Next Steps