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

Santwan PathakSantwan Pathak
7 min read

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:

  1. A Vanilla Node.js server (with manual routing using if-else)

  2. 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 homepage

  • If they visit /ice-tea → give them a virtual cup ☕

  • If they visit anything else → show a 404 - Not Found message


▶️ How to Run It

  1. Open your terminal

  2. Run the server:

node server-node.js
  1. Open your browser and go to:

🔄 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 with serve built-in

  • This function spins up a server instantly with minimal setup

fetch(req)

  • Instead of createServer((req, res) => {}), Bun uses a fetch()-like handler

  • If you've ever used fetch() in the browser, this will feel familiar

  • This 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 URL

  • Think 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 a Response 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:

URLResponse
http://127.0.0.1:3000/Hello, it's Ice Tea!
http://127.0.0.1:3000/ice-teaIce tea is a good option!
http://127.0.0.1:3000/random404 - 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:

FeatureNode.jsBun
Importsrequire("http")import { serve } from "bun"
Server creationhttp.createServer()serve()
Request objectreq.urlnew URL(req.url)
Responseres.writeHead(), res.end()return new Response(...)
Port/Host setupserver.listen(port, host)Included directly in serve({})
File supportCommonJS (.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. 😩

ProblemWhy It Matters
❌ Hard to readToo much nesting, unclear logic
❌ Not modularAll logic in one file — no separation
❌ Difficult to testNo 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.

0
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."