>_>_
Creating Your Own ExpressJS from Scratch (Part 1) - Basics, Methods, and Routing

Creating Your Own ExpressJS from Scratch (Part 1) - Basics, Methods, and Routing

Have you ever wondered how ExpressJS works internally? What happens behind the scenes when you define routes and handle requests? In this tutorial series, we'll demystify Express by building a simplified version of it step by step.

🧪 What You Will Learn

In this first part, we’ll build the foundation of our own Node.js web framework. You’ll learn how to:

  • Create and start an HTTP server
  • Define and match routes for different HTTP methods
  • Understand how URL patterns are matched using regular expressions
  • Lay the groundwork for middleware and controller handling

By building things from scratch, you’ll gain deeper insight into how web frameworks really work — and learn how to extend or debug them with confidence.


⚙️ Prerequisites

  • Node.js v16.16 or higher
  • Basic understanding of JavaScript and Node’s http module

Install the only external dependency we'll need:

npm install path-to-regexp

Then set up your project:

mkdir web-framework
cd web-framework
npm init -y

📁 Project Setup and Entry Point

src/app.js

This file defines the core logic of your custom framework.

Start by importing the necessary modules:

const { createServer } = require('http');
const { match } = require('path-to-regexp');

createServer lets us build a raw HTTP server. match is a helper from path-to-regexp for route matching.

Now define the main app function:

const App = () => {
  const routes = new Map();
  const createMyServer = () => createServer(serverHandler.bind(this));

🔧 Defining Routes by HTTP Method

We create functions to register routes with their handlers for GET, POST, PUT, PATCH, and DELETE.

  const get = (path, ...handlers) => {
    const currentHandlers = routes.get(`${path}/GET`) || [];
    routes.set(`${path}/GET`, [...currentHandlers, ...handlers]);
  };

  const post = (path, ...handlers) => {
    const currentHandlers = routes.get(`${path}/POST`) || [];
    routes.set(`${path}/POST`, [...currentHandlers, ...handlers]);
  };

  const put = (path, ...handlers) => {
    const currentHandlers = routes.get(`${path}/PUT`) || [];
    routes.set(`${path}/PUT`, [...currentHandlers, ...handlers]);
  };

  const patch = (path, ...handlers) => {
    const currentHandlers = routes.get(`${path}/PATCH`) || [];
    routes.set(`${path}/PATCH`, [...currentHandlers, ...handlers]);
  };

  const del = (path, ...handlers) => {
    const currentHandlers = routes.get(`${path}/DELETE`) || [];
    routes.set(`${path}/DELETE`, [...currentHandlers, ...handlers]);
  };

These route definitions store handlers in a Map, using a composite key of the form path/METHOD.

🧹 Sanitizing and Matching URLs

URLs often contain query strings or trailing slashes that can interfere with route matching. We need to normalize them:

  const sanitizeUrl = (url, method) => {
    const urlParams = url.split('/').slice(1);
    const [lastParam] = urlParams[urlParams.length - 1].split('?');
    urlParams.splice(urlParams.length - 1, 1);
    const allParams = [...urlParams, lastParam].join('/');
    return `/${allParams}/${method.toUpperCase()}`;
  };

  const matchUrl = (sanitizedUrl) => {
    for (const path of routes.keys()) {
      const urlMatch = match(path, { decode: decodeURIComponent });
      const found = urlMatch(sanitizedUrl);
      if (found) return path;
    }
    return false;
  };

🧠 The Server Request Handler

This function is passed to createServer to handle every incoming HTTP request.

  const serverHandler = async (req, res) => {
    const sanitizedUrl = sanitizeUrl(req.url, req.method);
    const match = matchUrl(sanitizedUrl);

    if (match) {
      const handlers = routes.get(match);
      console.log(handlers);
      res.statusCode = 200;
      res.end('Found');
    } else {
      res.statusCode = 404;
      res.end('Not found');
    }
  };

For now, we just log the matched handlers and return a basic success or error message.

🚀 Starting the Server

  const run = (port) => {
    const server = createMyServer();
    server.listen(port);
  };

  return {
    run,
    get,
    post,
    patch,
    put,
    del,
  };
};

module.exports = App;

🧪 Testing the Framework

Create a test file index.js:

const App = require('./src/app');

const app = App();

app.get('/test/test2', function test() {}, function test2() {});
app.post('/test', (req, res) => console.log('test'));
app.patch('/test', (req, res) => console.log('test'));
app.put('/test', (req, res) => console.log('test'));
app.del('/test', (req, res) => console.log('test'));

const start = async () => {
  app.run(3000);
};

start();

Now run:

node index.js

Use Postman, curl, or your browser to hit routes like:

GET http://localhost:3000/test/test2

📚 Summary

  • You've built a simple routing engine using Node's http module
  • Routes are matched using path-to-regexp
  • Each route can hold multiple handlers (middleware/controller support coming soon!)

✅ What’s Next?

In Part 2, we’ll build support for middlewares and controller chaining — taking our mini-framework even closer to ExpressJS.

Stay tuned, and happy hacking! 🚀