Building Web Servers with Node.js and Bun – A Backend Developer’s First Step


Welcome Back to the Backend Development Series!
In the previous post, we laid the groundwork:
✅ We explained what a web server is
✅ We explored the Client-Server Architecture
✅ We saw how JavaScript can run on the backend using Node.js and Bun
✅ And most importantly, we built our very first web server using Node’s built-in
http
module
But that server had one big limitation...
⚠️ The Problem With Our First Server
In our first server, no matter what URL you visited — whether it was /
, /about
, /ice-tea
, or even /random-nonsense
— the server would respond with the exact same message:
Hello from your first Node server!
Why? Because we didn’t check what URL the user was actually requesting.
We just responded blindly to every single request.
And that’s not how real-world websites work, right?
/login
should show a login form/about
should show info about your site/ice-tea
should confirm your tea order ☕
This brings us to an important feature in every backend system:
👉 Routing
What Is Routing?
Routing is the logic that tells your server:
➡️ “If a user visits this path (/
), respond with this content.
If they visit that path (/ice-tea
), respond differently.”
In simple terms:
Different URLs → Different responses
What You’ll Learn in This Blog
We’re going hands-on again — but this time, we’ll build two smart servers:
A Vanilla Node.js server (with manual routing using
if-else
)A Bun server (with its built-in speedy tools)
By the end, you'll understand:
✅ How to respond to specific paths like
/
,/login
,/ice-tea
✅ How routing works without any frameworks
✅ Why developer experience matters
✅ And why frameworks like Express.js were born to save our sanity
Let’s start with Node.js.
🛠️ Building a Smarter Web Server in Node.js (Vanilla Style)
📁 Step 1: Create a File
Create a new file named:
server-node.js
🧠 Step 2: Add This Code
const http = require("http");
const hostname = "127.0.0.1";
const port = 3000;
const server = http.createServer((req, res) => {
if (req.url === "/") {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Hello, it's Ice Tea!");
} else if (req.url === "/ice-tea") {
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain");
res.end("Thanks for ordering ice tea. It's really hot!");
} else {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain");
res.end("404 - Not Found");
}
});
server.listen(port, hostname, () => {
console.log(`Server is listening at http://${hostname}:${port}`);
});
🧩 What’s Happening Here?
Let’s break it down:
🔁 req.url
This gives us the path the user is trying to access. Examples:
/
/ice-tea
/something-weird
We use this value to decide what response to send back.
🧠 if...else if...else
This is basic routing logic:
If the user visits
/
→ show the homepageIf they visit
/ice-tea
→ give them a virtual cup ☕If they visit anything else → show a
404 - Not Found
message
▶️ How to Run It
Open your terminal
Run the server:
node server-node.js
- Open your browser and go to:
http://127.0.0.1:3000/
→ You’ll see:Hello, it's Ice Tea!
http://127.0.0.1:3000/ice-tea
→ You’ll see:Thanks for ordering ice tea. It's really hot!
http://127.0.0.1:3000/anything-else
→ You’ll see:404 - Not Found
🔄 Why This Matters
In our first server (from the last post), we didn’t handle routes at all.
The result? Every request got the same reply. That’s not practical.
This version adds conditional logic — a baby step toward how real websites handle pages, endpoints, and APIs.
You’ve just built your first routing mechanism in Node.js!
🟣 Rebuilding the Same Server with Bun
We’ve already built a basic web server in Node.js using http.createServer
and some conditional routing.
Now let’s recreate the exact same logic, but this time using Bun — the newer, faster JavaScript runtime that’s shaking up the backend ecosystem.
✅ If Node.js feels like a power drill, Bun is more like a Swiss Army knife — fast, modern, and loaded with built-in tools.
📁 Step 1: Create a File
Create a new file named:
server-bun.js
🧠 Step 2: Add This Code
import { serve } from "bun";
serve({
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello, it's Ice Tea!", { status: 200 });
} else if (url.pathname === "/ice-tea") {
return new Response("Ice tea is a good option!", { status: 200 });
} else {
return new Response("404 - Not Found", { status: 404 });
}
},
port: 3000,
hostname: "127.0.0.1"
});
🧩 What’s Going On Here?
Let’s break it down piece by piece:
✅ import { serve } from "bun"
No need for
http
module or third-party packages — Bun comes withserve
built-inThis function spins up a server instantly with minimal setup
✅ fetch(req)
Instead of
createServer((req, res) => {})
, Bun uses afetch()
-like handlerIf you've ever used
fetch()
in the browser, this will feel familiarThis function receives the request and is expected to return a
Response
object
✅ new URL(req.url)
This gives you an easy way to extract the
pathname
(like/
or/ice-tea
) from the request URLThink of it like a more modern and cleaner way than parsing
req.url
manually in Node
✅ return new Response(...)
- Just like the browser's
fetch()
API — you return aResponse
object with a body and status code
What You Just Did
You created a complete HTTP server using only 10 lines of code, with:
No external dependencies
No manual headers
No boilerplate setup
And it still behaves exactly like the Node.js version:
▶️ How to Run It
Make sure you have Bun installed. Then run:
bun server-bun.js
Now, visit these URLs in your browser:
URL | Response |
http://127.0.0.1:3000/ | Hello, it's Ice Tea! |
http://127.0.0.1:3000/ice-tea | Ice tea is a good option! |
http://127.0.0.1:3000/random | 404 - Not Found |
Key Learnings – Node.js vs Bun Syntax Comparison
Let’s break down how Node.js and Bun differ in terms of syntax and developer experience:
Feature | Node.js | Bun |
Imports | require("http") | import { serve } from "bun" |
Server creation | http.createServer() | serve() |
Request object | req.url | new URL(req.url) |
Response | res.writeHead(), res.end() | return new Response(...) |
Port/Host setup | server.listen(port, host) | Included directly in serve({}) |
File support | CommonJS (.js ) | ESM-first (.js , .ts , .jsx , etc.) |
Auto Restart | ❌ Manual restart required | ❌ Not built-in (use bunx nodemon or bun --watch ) |
⚠️ The Problem: Code Reusability & Scalability
Let’s face it — even with just 3 routes, our if-else
chain already looks messy.
Imagine managing 20+ routes in a real-world API this way. 😩
Problem | Why It Matters |
❌ Hard to read | Too much nesting, unclear logic |
❌ Not modular | All logic in one file — no separation |
❌ Difficult to test | No reusable structure, can't isolate parts |
The Solution: Use a Framework
That’s why developers turn to backend frameworks — to make their code cleaner, modular, and scalable.
Some popular options:
🟩 Express (Node.js) – industry standard
🟣 Hono (Bun) – minimal, super fast
🟦 Fastify – fast, schema-based
🔶 Koa – modern, middleware-centric
These frameworks provide:
✅ Route grouping
✅ Middleware support (auth, logging, etc.)
✅ Centralized error handling
✅ Clean, readable syntax
✅ Better DX (Developer Experience)
What’s Next
?
In the next blog, we’ll take things to the next level:
Explore backend frameworks (not just Express)
Start using Express.js to build cleaner, modular, and testable APIs
Learn about middleware, route grouping, and error handling
You’ve just taken your first real step toward backend mastery.
If you found this post helpful, just wait till we dive into frameworks — things are about to get way more powerful (and easier)!
Stay tuned.
Subscribe to my newsletter
Read articles from Santwan Pathak directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Santwan Pathak
Santwan Pathak
"A beginner in tech with big aspirations. Passionate about web development, AI, and creating impactful solutions. Always learning, always growing."