Building a ServerIntermediate8 min05 / 8

Building an HTTP Server

Learn how to spin up your own web server using Node's built-in http module — no frameworks needed.

Every website you visit is powered by a server — a program that listens for requests and sends back responses. Up until now you might have thought servers were mysterious, complex things reserved for back-end wizards. The truth is that Node.js ships with everything you need to build one, right out of the box, using the built-in http module. By the end of this lesson you will have a real, working HTTP server running on your own machine.

#What is HTTP, really?

Think of it like

A restaurant analogy

Think of HTTP like a restaurant. A client (your browser) walks in and places an order (the request). The kitchen (your server) reads the order, prepares the food, and hands it back on a plate (the response). HTTP is just the agreed-upon language for placing orders and delivering food — it defines the format for both the request and the response.

Every HTTP conversation has two sides:

  • Request — sent by the client. It includes a method (like GET or POST), a URL path (like /about), optional headers, and sometimes a body.
  • Response — sent by the server. It includes a status code (like 200 OK or 404 Not Found), headers, and a body (like HTML, JSON, or plain text).

Node's http module lets you write the kitchen side of this conversation.

#Importing the http module

http is built into Node — no npm install needed.
const http = require('http');

console.log(typeof http.createServer); // 'function'

http is a core module, meaning it is bundled with Node itself. You never have to install it. Just require it and you are ready to go.

#Creating a server with http.createServer

http.createServer() accepts a request listener — a callback function that Node calls every single time a client sends a request. The callback receives two objects: req (the incoming request) and res (the outgoing response you will write to).

A minimal but complete HTTP server in 8 lines.
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, world!');
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

Let's break that down piece by piece:

  1. http.createServer(callback) — creates the server object but does not start listening yet.
  2. res.writeHead(statusCode, headers) — sends the HTTP status line and headers to the client.
  3. res.end(body) — sends the response body and signals that the response is complete. You must call this or the client will hang forever waiting.
  4. server.listen(port, callback) — tells Node to start accepting connections on the given port. The optional callback fires once the server is ready.

#Reading the request: url and method

The req object is a goldmine of information. The two most important properties for routing are req.url and req.method.

Basic manual routing using req.url and req.method.
const http = require('http');

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);

  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>Welcome home!</h1>');
  } else if (req.method === 'GET' && req.url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>About us</h1>');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('404 Not Found');
  }
});

server.listen(3000);
Tip

req.url includes the query string

If the browser visits /search?q=cats, then req.url is the full string '/search?q=cats'. To parse it cleanly, use Node's built-in URL class:

``js const { pathname, searchParams } = new URL(req.url, 'http://localhost'); // pathname => '/search' // searchParams.get('q') => 'cats' ``

#Writing the response

The res object gives you a few key tools:

| Method | What it does | |---|---| | res.writeHead(code, headers) | Set the status code and response headers | | res.setHeader(name, value) | Set a single header (can be called before writeHead) | | res.write(chunk) | Send a chunk of the body (can be called multiple times) | | res.end(chunk?) | Finish the response, optionally sending one last chunk |

A common pattern is to skip res.write() and pass everything to res.end() in one shot, which is fine for small responses.

Serving JSON — the bread and butter of REST APIs.
const http = require('http');

const server = http.createServer((req, res) => {
  const data = { message: 'Hello from Node!', path: req.url };

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
});

server.listen(3000);
Common mistake

Always set Content-Type

If you forget Content-Type, browsers and API clients may misinterpret your response. Returning HTML? Use text/html. Returning JSON? Use application/json. Returning plain text? Use text/plain. The header tells the client how to parse the body.

Watch out

You cannot modify headers after res.end()

Once you call res.end() (or res.write()), the headers are already sent to the client. Trying to call res.writeHead() or res.setHeader() after that will throw an error. Always set your headers first.

#Putting it all together

A complete server handling two routes and a 404 fallback.
const http = require('http');

const PORT = 3000;

const server = http.createServer((req, res) => {
  const { method, url } = req;

  if (method === 'GET' && url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>Home</h1><p>Welcome!</p>');
  } else if (method === 'GET' && url === '/api/greet') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ greeting: 'Hello!' }));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not found');
  }
});

server.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
Quick check

Which line actually starts accepting incoming connections from clients?

Key takeaways

  • Node's built-in http module lets you create a server with zero external dependencies.
  • http.createServer() takes a callback with (req, res) that runs on every request.
  • Use req.url and req.method to decide how to respond, and res.writeHead() + res.end() to send the response.
  • Always set Content-Type so clients know how to interpret your response body.
  • server.listen(port) is the final step that opens the port and starts accepting connections.
Practice challenges
Test yourself · earn XP
0/4
Predict the output#1

A client sends GET /about to this server. What does the server write to the console?

predict-output
const http = require('http');

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('ok');
});

server.listen(3000);
Fix the bug#2

This server is supposed to reply 'Hello, world!' to every request, but clients just hang forever with no response. What's wrong?

fix-bug
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('Hello, world!');
});

server.listen(3000);
Fill in the blank#3

Complete this line so the server sends a JSON response body. Fill in the method that converts the object into a string for res.end().

const data = { message: 'Hello from Node!' };
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end((data));
Reorder the lines#4

Put these lines in the correct order to build a minimal HTTP server that replies with plain text and then starts listening on port 3000.

1
const http = require('http');
2
server.listen(3000);
3
  res.writeHead(200, { 'Content-Type': 'text/plain' });
4
  res.end('Hello, world!');
5
});
6
const server = http.createServer((req, res) => {
Your turn
Practice exercise

Create an HTTP server that listens on port 4000. It should handle three routes: 1. GET / — respond with plain text: 'Welcome to my server!' 2. GET /time — respond with JSON containing the current time, e.g. { "time": "2024-01-01T12:00:00.000Z" } 3. Any other request — respond with a 404 status and the text 'Not found'

Make sure to set the correct Content-Type header for each route.

Try it yourself — a starting point to build on:

starter.js
const http = require('http');

const server = http.createServer((req, res) => {
  // TODO: check req.method and req.url
  // Route 1: GET /  -> 200, text/plain, 'Welcome to my server!'
  // Route 2: GET /time -> 200, application/json, { time: <ISO string> }
  // Fallback: 404, text/plain, 'Not found'
});

server.listen(4000, () => {
  console.log('Server running on http://localhost:4000');
});