
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! 🚀