3. Server Basics with node:http
Follow along with code examples here!
Now that you know how the internet works, it's time to run your own server. Before jumping into Express, we'll build one using Node's built-in node:http module — no npm packages required. This forces you to confront what a server actually is: code that listens for incoming requests, inspects them, and constructs responses manually.
By the end, the complexity of doing this by hand will make the purpose of Express obvious.
We'll also use curl, a command-line tool for sending HTTP requests, to see exactly what's happening at the protocol level.
Table of Contents:
Essential Questions
By the end of this lesson, you should be able to answer these questions:
What does it mean for a server to be "listening"?
What are
hostandport? How do they together identify a server?What is
localhost? Why do we use it during development?What does
http.createServer()do? What are thereqandresobjects?How do you inspect the URL and method of an incoming request?
How do you manually construct and send an HTTP response with
node:http?What does doing all of this by hand reveal about why a framework like Express is useful?
Terms
Listening — a server is "listening" when it is actively waiting for incoming HTTP requests on a specified port.
Host — the network address of a machine. During development, we use
localhost(which means "this computer").Port — a number that identifies a specific process or application on a host. Different applications run on different ports.
localhost— a hostname that always refers to your own computer. It resolves to the IP address127.0.0.1.node:http— Node's built-in module for creating HTTP servers and making HTTP requests. No installation required.http.createServer()— creates an HTTP server. The callback function you pass is called once for every incoming request.req(IncomingMessage) — the request object. Contains everything about the incoming request: URL, method, headers, body.res(ServerResponse) — the response object. Has methods for setting headers and writing the response body back to the client.res.writeHead()— sets the HTTP status code and response headers.res.end()— sends the response body and signals that the response is complete.curl— a command-line tool for sending HTTP requests and seeing the raw response.Routing — deciding what to do with a request based on its URL and method.
What Does "Listening" Mean?
In the previous two lessons, we learned that servers receive requests and send responses. But a server doesn't just passively wait — it actively listens for incoming connections on a specific port.
Think of it this way: your computer has thousands of ports, like doors into an apartment building. Each door can be used by a different application to receive requests. When a server application starts listening on port 8080, it's like opening that specific door and saying: "I'm here. Send requests to this door."

When your server is running locally, the host is localhost (your own computer) and the port is whatever number you chose. Together, localhost:8080 uniquely identifies each server application on your machine.
Q: Why can't two applications listen on the same port at the same time?
Because the operating system uses the port number to decide which application receives incoming data. If two applications tried to listen on the same port, the system wouldn't know which one to send the data to. This is why you get an error like "address already in use" if you try to start a server on a port that's already taken. The fix is either to stop the other process or use a different port number.
Your First Server with node:http
node:httpNode ships with a built-in http module. You don't need to install anything — just require it.
Creating the Server
Run it:
Visit http://localhost:8080 in your browser. You'll see:
That's your server receiving an HTTP request and sending back a JSON response — entirely in Node, with zero dependencies.
http.createServer() takes a request listener — a callback function that is called once for every incoming request. Every single request your server receives, no matter the URL or method, will invoke this callback.
The Request Object
The first argument to the callback — conventionally named req — is an object containing everything about the incoming request.
The three most important properties are:
req.method— the HTTP method of the requestreq.url— the path portion of the URL (everything after the host and port)req.headers— an object of all request headers
Q: You visit http://localhost:8080/api/users?sort=asc. What is the value of req.url?
'/api/users?sort=asc' — req.url includes the pathname (/api/users) and the query string (sort=asc). If you want just the pathname, you need to parse it which can be done using the new URL() constructor:
This is one of many things frameworks like Express can handle automatically for you.
The Response Object
The second argument — res — is how you send a response back to the client. It has two key methods:
res.writeHead(statusCode, headers) — sets the status code and response headers. Must be called before res.end().
res.end(body) — sends the response body and closes the connection. The body must be a string or Buffer.
res.end() must be called for every request, on every code path. Try removing it to see what happens!
If you forget to call it, the client will hang indefinitely waiting for a response.
Q: What happens if you call res.end() twice for the same request?
Node will throw an error: Error: write after end. Once you've called res.end(), the response is closed — you can't write to it again. This is a common mistake in branching code where multiple code paths could reach a res.end() call. Use early return statements to prevent it:
Let's see it in action with routing.
Routing Manually
Currently our server responds the same way to every request — any URL, any method. Real servers respond differently based on the requested endpoint. This is called routing.
With node:http, routing means inspecting req.url and req.method and branching with if/else:
Q: How would you add a route that only responds to POST /api/todos?
Add another if block before the catch-all 404. This assumes that the POST body is a JSON object in the format { "task": "description" }:
Notice that reading the request body requires listening to stream events (data, end) and assembling the body from the chunks of stream data. This is another thing Express handles automatically via express.json() middleware.
Sending Requests with curl
curlWhile a browser is great for seeing rendered pages, it hides a lot of what's happening at the HTTP level. curl is a command-line tool that sends raw HTTP requests and shows you the raw response — no rendering, no UI, just the protocol.
Basic Requests
With your server running, open a new terminal tab:
Output:
Output:
Output:
Viewing Headers
Use the -i flag to include response headers in the output:
Output:
Notice:
The status line:
HTTP/1.1 200 OKResponse headers:
Content-Type,Date,ConnectionA blank line separating headers from the body
The JSON body
This is the raw HTTP response — the same data your browser receives, before it does anything with it.
Sending a POST Request
Use -X to set the method, -H to add a header, and -d to send a body:
Output:
Q: What is the difference between using curl and opening the URL in a browser to test your server?
A browser always sends
GETrequests when you type a URL. It can't easily sendPOST,PATCH, orDELETEwithout JavaScript. It also automatically processes and renders the response.curlgives you full control over the method, headers, and body. It shows you the raw response — headers and all — with no processing. It's essential for testing non-GET endpoints and seeing exactly what the server sends.
Later, we'll use Postman — a GUI tool that gives you curl's flexibility with a more comfortable interface.
Q: What does the Content-Type: application/json header in a request tell the server?
It tells the server that the request body is formatted as JSON and should be parsed accordingly. Without this header, the server doesn't know how to interpret the raw bytes it receives in the body. Express's express.json() middleware uses this header to decide whether to JSON-parse the request body automatically.
Why Express?
Look at what we had to do manually with node:http:
Routing — write
if/elsechains to match every URL and method combinationReading the request body — listen to stream events (
data,end), accumulate chunks, and parse manuallySetting headers on every response — call
res.writeHead()every single timeJSON serialization — call
JSON.stringify()on every outgoing objectPath parameters — parse something like
/api/todos/3yourself to extract the3Query strings — manually construct a
URLobject to parse?sort=ascError handling — build your own pattern from scratch
None of these are difficult in isolation. But combined across an entire application with dozens of endpoints, the boilerplate becomes overwhelming and error-prone.
Express is a framework that handles all of this for you. The request listener pattern you just learned — (req, res) => { ... } — is exactly how Express works internally. Express wraps node:http, enhances req and res with useful properties and methods, and gives you a clean way to define routes and middleware.
You now understand what's happening underneath. Next lesson, we build with Express.
Q: Since Express is built on top of node:http, does that mean you could build anything Express can build using just node:http?
Yes — Express doesn't add any new capabilities that node:http doesn't have. Everything Express does, you could write yourself with node:http. Express just removes the repetitive work. This is the point of a framework: it doesn't expand what's possible, it makes common patterns faster and less error-prone to implement. Understanding node:http first means you're never confused by what Express is "doing behind the scenes" — you've already seen the raw version.
Last updated