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
- How http.createServer and server.listen build a server
- Why the handler runs once per request, with req and res
- How to set a status code with res.statusCode / res.writeHead
- How to add headers with res.setHeader and send JSON
- How to read req.method and req.url to know what was asked
- How to collect a POST body from the request stream
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 hereCommon Errors (and the fix)
- The request hangs forever — You almost certainly forgot res.end(). Without it Node never finishes the response, so the browser or curl just spins. Every code path through your handler must call res.end(...) exactly once.
- "EADDRINUSE: address already in use :::3000" — Another program (often an old copy of this server you forgot to stop) is already holding port 3000. Stop the other process, pick a different port, or find and kill it (lsof -i :3000 on macOS/Linux).
- "Cannot set headers after they are sent to the client" — You called res.setHeader or res.writeHead after res.end (or called res.end twice). Set the status and all headers first, then end the response last.
- JSON shows up as plain text (or won't parse) — You sent JSON.stringify(obj) but forgot the header. Add res.setHeader('Content-Type', 'application/json') so the client treats it as JSON.
- The server "won't stop" — That's normal: a server is meant to keep running. Press Ctrl+C in the terminal where it's running to shut it down.
Pro Tips
- 💡 Read the port from the environment: use const port = process.env.PORT || 3000 so hosting platforms can choose the port for you.
- 💡 Always send a Content-Type: it tells clients how to read your body. text/plain, text/html, and application/json cover most early cases.
- 💡 Frameworks build on this: Express and friends are thin layers over exactly this req/res model — learning the raw version makes them click instantly.
📋 Quick Reference — Node.js Servers
| Concept | Node Syntax |
|---|---|
| Create a server | http.createServer(fn) |
| Start listening | server.listen(3000) |
| Status + headers | res.writeHead(200, headers) |
| Set one header | res.setHeader(k, v) |
| Send body + close | res.end(body) |
| Request method | req.method |
| Request path | req.url |
🎉 Lesson Complete!
- ✅ http.createServer(fn) builds a server; the handler runs once per request
- ✅ server.listen(port) starts it — and keeps it running until Ctrl+C
- ✅ res.writeHead(status, headers) or res.statusCode + res.setHeader shape the reply
- ✅ Send JSON with JSON.stringify and a application/json Content-Type
- ✅ req.method and req.url tell you what was asked; a POST body streams in via 'data'/'end'
- ✅ Always finish with res.end() or the request hangs
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
- Previous: The http Module
- Next: Routing by Hand — Send different responses based on the URL and HTTP method
- Quick reference: Node.js cheat sheet › Core Modules (fs, path, http)
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.