Introduction
When building HTTP Servers, Express.js is one of the most popular frameworks developers use to create HTTP servers quickly, simplifying the process of building web applications and APIs. With its minimal setup, middleware support, and routing system, developers can quickly create powerful backend applications.
But What if Express.js never existed?
Would we be able to write HTTP Servers? The answer is Yes. But without Express, developers would have to manually handle routing, request parsing, middleware management, and other core functionalities that Express abstracts away. This means more boilerplate code, increased complexity, and a steeper learning curve for newcomers to backend development.
So what is the other way to build HTTP Servers?
How HTTP Servers Work
Before diving directly into writing an HTTP Server. Let’s first understand the Basics of How HTTP Servers Work.
Think of an HTTP server as a waiter in a restaurant. The client (you) places an order (request), the waiter takes it to the kitchen (server), and the kitchen prepares the meal (response) and sends it back to you. In web development, this process happens millions of times a second, serving everything from web pages to APIs.
The key components of HTTP servers:
-
Requests: What the client asks for (e.g., a webpage, data, or an image).
-
Responses: What the server sends back (e.g., HTML, JSON, or an error message).
-
The Role of a Server: The server processes the request, performs any necessary actions (e.g., fetching data from a database), and sends back a response.
Request-Response Cycle

Example of an HTTP Request and Response:
-
Request:
-
Method:
GET -
URL:
/about -
Headers:
{ "Accept": "text/html" }
-
-
Response:
-
Status Code:
200 OK -
Headers:
{ "Content-Type": "text/html" } -
Body:
<h1>About Us</h1>
-
Building an HTTP Server in Node.js
Imagine you're cooking a meal, but you have no kitchen tools no knives, no pre-cut vegetables, and no stove. You'd have to do everything manually from chopping to cooking.
Building a server in raw Node.js is just like that you have to handle everything yourself. Express.js, on the other hand, is like a fully-equipped kitchen with pre-cut ingredients, a stove, and a recipe book, making the process much faster.
let’s first cook from scratch by building a simple HTTP server in Node.js step by step.
Step 1: Import the http module
Unlike Express, where you just call express(), here we manually import Node.js’s built-in http module, which allows us to create an HTTP server.
import http from "http";
Step 2: Create a Server
In express, you could simply do app.get('/', (req, res) => res.send("Hello World")). But in Node.js, We need to create a server first. Node.js’s built-in http module has some methods, createServer() is one of them that let’s you create a server that listens for incoming requests. It takes a callback as an argument which has two parameters req and res.
1. req (Request Object)
-
Represents the incoming HTTP request from the client.
-
Contains details such as the URL, HTTP method, headers, and request body.
-
Common properties:
-
req.method→ The HTTP method (GET,POST, etc.). -
req.url→ The requested URL. -
req.headers→ An object containing request headers. -
req.on("data", callback)→ Used to read request body data (forPOST/PUTrequests).
-
2. res (Response Object)
-
Represents the outgoing HTTP response sent to the client.
-
Used to send response data, set headers, and status codes.
-
Common methods:
-
res.writeHead(statusCode, headers)→ Sets response status and headers. -
res.write(data)→ Writes data to the response body. -
res.end(data)→ Ends the response (optionally sending final data).
-
const server = http.createServer((req, res) => { res.end('Hello, World!'); // express -> res.send & Node -> res.end });
Now, The server needs to listen on some Port
Step 3: Listen on a Port
The “server” variable that we have created. it has a method called server.listen().
server.listen(3000, () => { console.log("Server listening at port: 3000"); });
Output:

This is How you create a basic server in Node.js using http module. But how do we handle Routes?
Handling Routes in Node.js
Normally, in Express.js, you can define routes like this:
app.get("/", (req, res) => res.send("Home Page")); app.get("/about", (req, res) => res.send("About Page"));
But in Node.js, There is no app.get() or server.get() , Here we must manually handle the routes using req.url property . Let’s see how that works.
import http from "http"; const port = 3000 const server = http.createServer((req, res) => { if (req.url === '/') { // req.url -> Captures the URL requested by the client. res.end('Home Page'); } else if (req.url === '/about') { res.end('About Page'); } else { res.end('Page Not Found'); } }); server.listen(port, () => { console.log(`Server listening on port: ${port}`); });



Do you see the problem here? Every time we add a new route, we have to manually check it using multiple if-else statements. Imagine handling dozens of routes like this, it would quickly become messy and hard to maintain. This was one of the pain point of developers The “if-else Hell”. Although the developers did come up with approaches like creating a router object (you can google it). But still it was a complex process.
Middlewares
Now that we have routes, let's talk about Middlewares.
What is Middleware?
Middleware is just a function that runs before the actual request is handled. Think of it like a security guard checking IDs before letting people into an event.
Without middleware:
• Request → Server → Response
With middleware:
• Request → Middleware → Server → Response
Node.js doesn’t have built-in middleware handling like Express does. So, let’s see how we can do it manually.
import http from "http"; const port = 3000; // Middleware function: Simulating an authentication check const authCheck = (req, res) => { console.log("Checking authentication..."); // Simulating authentication logic if (req.headers["authorization"] !== "secret-token") { res.end("Unauthorized Access"); return false; } return true; }; const server = http.createServer((req, res) => { // Middleware: Check authentication before handling the request if (!authCheck(req, res)) return; res.end("Welcome, User"); }); server.listen(port, () => { console.log(`Server listening on port: ${port}`); });
Now you might ask, Mukul why didn’t we use next() function?
Great question sir, In raw Node.js, there is no next() function like in Express.
In Express, middleware functions receive req, res, and next. The next() function tells Express to move to the next middleware in the chain.
But in pure Node.js, we have to manually control the flow by checking conditions and returning responses.
How Does Flow Work Without next()?
In above example, this line stops execution if authentication fails:
if (!authCheck(req, res)) return;
Since there's no Express-style middleware chain, we just return early to prevent further code execution.
Conclusion
I hope you enjoyed this article and picked up some core Node.js concepts along the way. We explored how HTTP servers work, how to handle routes, and how to use middleware all from scratch.
But this is just the beginning! There’s still so much more to explore, like:
• Handling route parameters and query strings
• Processing JSON and URL-encoded data
• Managing error handling and authentication
I’ll leave that to your curiosity and practice because the best way to learn is to build!
And i hope you understood the major pain points of writing everything from scratch and why frameworks like Express exists which makes the life of a developer so much easier. This was the whole motive of this article, to make you understand the importance of Express. We all were seeing express as just a tool for writing HTTP Servers. But now you understood that express is nothing but just a framework built on top of Node.js’s “HTTP” Module. Which makes it so much easier to write a web server abstracting all the complexities.
Assignment
NOT DONE YET…
I’ve some assignments for you:
Assignment 1: Basic HTTP Server with Routing
Create a simple Node.js server that:
-
Listens on port 3000
-
Handles three routes:
-
/→ Responds with"Welcome to my server!" -
/about→ Responds with"This is the About page" -
/contact→ Responds with"Contact us at:example@email.com"
-
-
Returns a 404 error for any other route
Bonus: Implement an object-based routing system instead of using if-else.
Assignment 2: Implement a Request Logger Middleware
Create a middleware function that:
-
Logs the request method and URL (e.g.,
GET /about) -
Calls the next function in the request cycle
-
Applies this middleware to all routes
Bonus: Extend the middleware to log timestamps and save logs to a file (logs.txt).
Assignment 3: JSON Data Handling
Create a POST route (/data) that:
-
Accepts JSON data (e.g.,
{ "name": "Mukul" }) -
Responds with
"Hello, Mukul!" -
If no name is provided, responds with
"Hello, Guest!"
Bonus: Validate that the request body contains a valid JSON format before processing.
Some Learning Sources
-
Official Node.js Docs: https://nodejs.org/docs/latest/api/http.html
-
MDN Web Docs on HTTP: https://developer.mozilla.org/en-US/docs/Web/HTTP
-
Deep Dive into Express.js: https://expressjs.com/
Connect with Me
-
X (Twitter): mukul033
-
GitHub: mukulpal03
-
Blog: Mukul's Dev Journal