Building a Server

Reviewed & published by Brayan K

A Node.js HTTP server is a long-running program created with http.createServer that listens on a port and runs a handler function for every incoming request, sending back a response you control.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In the last lesson you met the http module. Now you'll use it to build a real server: one that starts up, listens on a port, reads what the client asked for, and sends back exactly the status, headers, and body you choose.

What You'll Learn in This Lesson

1️⃣ The Simplest Possible Server

A working Node server is only a few lines. You give http.createServer a handler function, then call server.listen(port) to start it. The handler receives two objects every time a request arrives: req (what the client asked for) and res (the reply you build). Read every comment below, then run it.

// Pull in Node's built-in http module — no install needed.
const http = require('http');

// createServer takes ONE handler function. Node calls it for
// EVERY request that arrives. req = the incoming request,
// res = the response you build and send back.
const server = http.createServer((req, res) => {
  // res.end() sends the body AND closes the response. Without
  // it the browser/curl just hangs forever waiting for more.
  res.end('Hello from Node!');
});

// Nothing happens until the server starts LISTENING on a port.
// The callback fires once it's ready. The server then stays
// running, waiting for requests, until you stop it.
const port = 3000;
server.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`);
});

Save that as server.js and start it. Unlike a normal script, it doesn't exit — it stays running, waiting for requests, until you stop it with Ctrl+C.

# Start the server (it keeps running in this terminal):
$ node server.js
Server listening on http://localhost:3000

# In a SECOND terminal, send it a request with curl:
$ curl http://localhost:3000
Hello from Node!

# Or just open http://localhost:3000 in your browser.
# Press Ctrl+C in the first terminal to stop the server.

2️⃣ Setting the Status Code and Headers

Every response carries a status code (200 means OK, 404 means not found) and headers (metadata about the body). You can set both in one shot with res.writeHead(status, headers), which must run before res.end.

const http = require('http');

const server = http.createServer((req, res) => {
  // Option A — set the status code and headers in one call.
  // writeHead(statusCode, headersObject) must run BEFORE res.end.
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Plain text, status 200');
});

server.listen(3000, () => console.log('Listening on 3000'));

Or set them one at a time with res.statusCode and res.setHeader(key, value). To send JSON, turn your object into a string with JSON.stringify and set Content-Type to application/json so the client knows how to read it.

const http = require('http');

const server = http.createServer((req, res) => {
  // Option B — set things one at a time. Same result, more control.
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');

  // To send JSON you must turn a JS object into a STRING first.
  // JSON.stringify does exactly that.
  const data = { message: 'Hello, JSON!', ok: true };
  res.end(JSON.stringify(data));
});

server.listen(3000, () => console.log('Listening on 3000'));

3️⃣ Reading the Incoming Request

So far we've ignored what the client actually asked for. The req object tells you: req.method is the verb (GET, POST, …) and req.url is the path. A request body (sent with a POST) doesn't arrive all at once — it streams in as chunks, so you collect it on the 'data' event and act on it when 'end' fires.

const http = require('http');

const server = http.createServer((req, res) => {
  // req.method is the HTTP verb: 'GET', 'POST', 'PUT', ...
  // req.url is the path that was requested, e.g. '/users?id=7'
  console.log(`${req.method} ${req.url}`);

  if (req.method === 'POST') {
    // A request body arrives in CHUNKS over a stream, not all at
    // once. Collect the chunks as they come in...
    let body = '';
    req.on('data', (chunk) => { body += chunk; });

    // ...then the 'end' event fires when the whole body is here.
    req.on('end', () => {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ youSent: body }));
    });
  } else {
    res.end(`You asked for ${req.url}`);
  }
});

server.listen(3000, () => console.log('Listening on 3000'));

Your turn. The handler below is almost done — fill in the three blanks marked ___ so it replies with the JSON object {message:'ok'}. Follow the 👉 hints, then run it and compare with the expected output.

const http = require('http');

// 🎯 YOUR TURN — finish the handler so it replies with JSON.
const server = http.createServer((req, res) => {
  // 1) Set the status code to 200 (OK)
  res.statusCode = ___;                          // 👉 the success code

  // 2) Tell the client the body is JSON
  res.setHeader('Content-Type', '___');          // 👉 application/json

  // 3) Turn this object into a string and send it
  const payload = { message: 'ok' };
  res.end(___);                                  // 👉 JSON.stringify(payload)
});

server.listen(3000, () => console.log('Listening on 3000'));

// ✅ Expected — curl http://localhost:3000
//    {"message":"ok"}

Mini-Challenge: A Two-Route Server

No blanks this time — just a brief and an outline. Using only req.url and if statements (no router yet — that's the next lesson), build a server that answers two paths and 404s everything else. Write it yourself, run it, and check your output against the example in the comments.

const http = require('http');

// 🎯 MINI-CHALLENGE: a tiny two-route server (no router yet).
// 1. Create a server with http.createServer.
// 2. If req.url is '/' respond with the text "Home".
// 3. If req.url is '/about' respond with the text "About us".
// 4. For anything else, send status 404 and the text "Not found".
// 5. Listen on port 3000 and log a message when it starts.
//
// ✅ Example output:
//    curl localhost:3000/        -> Home
//    curl localhost:3000/about   -> About us
//    curl localhost:3000/nope    -> Not found   (status 404)

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Node.js Servers

ConceptNode Syntax
Create a serverhttp.createServer(fn)
Start listeningserver.listen(3000)
Status + headersres.writeHead(200, headers)
Set one headerres.setHeader(k, v)
Send body + closeres.end(body)
Request methodreq.method
Request pathreq.url

🎉 Lesson Complete!

Practice quiz

Which built-in module creates an HTTP server in Node?

  • http
  • fs
  • net-server
  • express

Answer: http. The built-in http module's createServer builds a server with no install needed.

How many handler functions does http.createServer take?

  • One handler, called for every request
  • One per route
  • Two: request and response handlers
  • None

Answer: One handler, called for every request. createServer takes a single handler that Node calls for every incoming request.

What starts the server actually listening for requests?

  • server.start()
  • server.open()
  • server.listen(port)
  • server.run()

Answer: server.listen(port). Nothing happens until server.listen(port) begins listening on a port.

What does res.end() do?

  • Restarts the server
  • Logs the request
  • Opens a new connection
  • Sends the body and closes the response

Answer: Sends the body and closes the response. res.end() sends the body and finishes the response; without it the request hangs.

Which call sets the status code and headers together, before res.end?

  • res.writeHead(status, headers)
  • res.send(status)
  • res.header(status)
  • res.status(headers)

Answer: res.writeHead(status, headers). res.writeHead(statusCode, headersObject) sets both at once and must run before res.end.

To send a JS object as JSON, what must you call first?

  • JSON.parse(obj)
  • JSON.stringify(obj)
  • obj.toJSON only
  • res.json(obj)

Answer: JSON.stringify(obj). res.end sends strings, so JSON.stringify converts the object into a JSON string.

Which property tells you the HTTP verb of the request?

  • req.url
  • req.verb
  • req.method
  • req.path

Answer: req.method. req.method holds the verb such as GET or POST; req.url holds the path.

How does a POST request body arrive in the raw http handler?

  • All at once on req.body
  • As chunks via 'data' events, finalized on 'end'
  • As a query string
  • In req.params

Answer: As chunks via 'data' events, finalized on 'end'. The body streams in as chunks you collect on 'data', then act on when 'end' fires.

What does the error 'EADDRINUSE :::3000' mean?

  • The handler crashed
  • JSON failed to parse
  • Another program already holds port 3000
  • The body was too large

Answer: Another program already holds port 3000. EADDRINUSE means the port is already in use, often by an old copy of the server.

What header tells the client a response body is JSON?

  • Content-Type: application/json
  • Accept: text/plain
  • Content-Length
  • X-JSON: true

Answer: Content-Type: application/json. Setting Content-Type to application/json tells clients to parse the body as JSON.

Continue this course

Frequently asked questions

Why does my server keep running instead of finishing like a normal script?

That's by design. http.createServer plus server.listen starts a long-running process that sits and waits for incoming requests — a normal script runs top to bottom and exits, but a server has to stay alive so it can answer the next request at any moment. It only stops when you press Ctrl+C, the process is killed, or you call server.close(). If you expected it to exit, you were probably thinking of a one-off script; a server is meant to keep listening.

What is the difference between req and res in the handler?

req (the request) is read-only information about what the client asked for: req.method tells you the HTTP verb (GET, POST, ...), req.url tells you the path, and the body arrives as a stream of data events. res (the response) is what you build and send back: you set res.statusCode, add headers with res.setHeader or res.writeHead, and finish with res.end(body), which sends the data and closes the connection. In short, you read from req and write to res.

What port should I use, and what does the number mean?

A port is just a numbered door on your machine that lets the operating system route traffic to the right program. For local development 3000, 8000, and 8080 are popular, easy-to-remember choices. Avoid ports below 1024 (like 80 and 443) during development because they usually need admin/root privileges. In production you typically read the port from an environment variable — process.env.PORT — so the hosting platform can tell your app which door to open.

Why do I need JSON.stringify to send JSON, and Content-Type for it?

res.end can only send strings or buffers, not raw JavaScript objects, so JSON.stringify converts your object into a JSON-formatted string the network can transmit. Setting the Content-Type header to application/json then tells the client how to interpret those bytes — browsers and tools use it to parse the response back into an object automatically. Skip the stringify and you'll get an error or '[object Object]'; skip the header and clients may treat valid JSON as plain text.