Tier 4 — Advanced

Mini Web Framework

Build your own tiny Express: routes registered by path and dispatched by URL. Understand the magic by building it.

🐍 Python 🟨 JavaScript

1. The Problem

Build a minimal web framework. Handlers are registered against a URL path, and an incoming request path is dispatched to the matching handler. A known path returns status 200 with the handler's body; an unknown path returns 404.

Why this project? Express, Flask, and every web framework are, at their core, a routing table and a dispatcher. Building the smallest possible version demystifies "how does a URL become my function running?"

2. How to Think About This

Keep a Map of path → handler function. A route(path, handler) method registers one. handle(path) looks the path up: if a handler exists, call it and return [200, body]; if not, return [404, "Not Found"]. That is the entire essence of routing — everything else a real framework adds (methods, params, middleware) builds on this.

3. The Build

app.mjs
export class App {
  constructor() {
    this.routes = new Map();   // path -> handler
  }

  route(path, handler) {
    this.routes.set(path, handler);
    return this;   // chainable
  }

  handle(path) {
    const handler = this.routes.get(path);
    if (!handler) return [404, "Not Found"];
    return [200, handler()];
  }
}

const app = new App();
app.route("/", () => "Welcome home!")
   .route("/about", () => "About us");

console.log(app.handle("/"));         // [ 200, 'Welcome home!' ]
console.log(app.handle("/about"));    // [ 200, 'About us' ]
console.log(app.handle("/missing"));  // [ 404, 'Not Found' ]

4. Test & Prove

app.test.mjs
import { App } from "./app.mjs";

let pass = 0, fail = 0;
function test(desc, fn) {
  try { fn(); console.log("  \u2713", desc); pass++; }
  catch (e) { console.log("  \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);

test("a registered route returns 200 and its body", () => {
  const app = new App(); app.route("/hi", () => "hello");
  assert(eq(app.handle("/hi"), [200, "hello"]));
});
test("an unknown route returns 404", () => {
  assert(eq(new App().handle("/nope"), [404, "Not Found"]));
});
test("each route maps to its own handler", () => {
  const app = new App();
  app.route("/a", () => "A").route("/b", () => "B");
  assert(eq(app.handle("/a"), [200, "A"]) && eq(app.handle("/b"), [200, "B"]));
});
test("re-registering a path overrides the handler", () => {
  const app = new App();
  app.route("/x", () => "old").route("/x", () => "new");
  assert(eq(app.handle("/x"), [200, "new"]));
});

console.log(`\n${pass} passed, ${fail} failed`);

5. The Interface

node app.mjs

[ 200, 'Welcome home!' ]
[ 200, 'About us' ]
[ 404, 'Not Found' ]

6. Run It

node app.mjs
node app.test.mjs

🎯 Try this next

  1. Support HTTP methods — key routes by "GET /path" instead of just path
  2. Add URL parameters like /user/:id and pass them to the handler
  3. Add middleware: functions that run before every handler
  4. Wire it to Node\u2019s http module so it serves real requests

What you practised

Routing as a path→function map, dispatch with a lookup + fallback, returning a status/body pair, and method chaining by returning this.

Reference: Maps · Functions · Express

Try It Yourself