06 - HTTP from Scratch

📋 Jump to Takeaways

This 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.createServer gives you a server with zero dependencies
  • Every request gets req (what came in) and res (what you send back)
  • Always call res.end() or the request hangs
  • Route by checking req.method and req.url, that's all frameworks do
  • Request bodies arrive as streams, collect chunks with data and end events
  • In ESM, use import.meta.dirname instead of __dirname
  • Node 18+ has built-in fetch for making HTTP requests, no extra packages needed

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy