Build your own expressjs | Part 3
This blog post is part of a series in which we are going to build a minimal, simple and yet powerful version of Express.js, called Minimal.js. This is the third part. You can check out part 1 here and part 2 here.
We are learning on the go so if you find any mistake or any better way to do certain things or just want to share your feedback then I am all ears and open to collaboration. Let me know your opinions here.
The Road so far
So, till now, we talked about HTTP, explored a couple of Node.js modules, built a simple server using native modules and then we took a deep dive into how we can implement methods like use and middlewares support to our framework.
Part 3
This part would revolve around how routing works under the hood and implementation details regarding standard HTTP verbs like get, post and more.
I would recommend that you should code along. So, go ahead, clone the repo and check out the part-2 branch. Create a new branch part-3 from part-2.
git clone https://github.com/yomeshgupta/minimaljs.git
git checkout part-2
git checkout -b part-3 part-2
Complete code for this part can be found here.
Concepts
Routing System in Express, basically comprises of three major components:
- Router
- Layer
- Route
Let's talk about each but before that take a look at the diagram below.

Router
Router component is responsible for the entire routing of the application. It exposes various methods (HTTP verbs) like get, post, put and more; which are used by the application to bind handlers to particular paths. An instance of Router contains an array of Layers.
The relationship between Router and Layer is one-to-many i.e. an instance of a Router can have multiple Layers in it.
Layer
Each Layer consists of a path and a Route component which stores more specific information about that path.
Route
Route Component is the one where we implement the HTTP verbs. It contains the path and an array of Layers.
The difference between the Layer in the Router component and the Route component is that in the Route component, a Layer contains a method and its handler.
The relationship between path and method can be one-to-many i.e. a path like /login can have multiple methods attached to it.
GET /login
POST /login

In summary, a Router contains an array of layers where each layer represents a path and a route component. Each route component contains the same path along with all the method(s) handling associated with it. When a request is received, each Layer's path is matched against the current request path. If matched then we compare the current request method type with method handling provided at the time of setup, if it is also a match then the apt handler is executed else not found is returned.
Let's code
Create a folder router and create a file layer.js inside it.
cd ./src
mkdir router
cd ./router
touch layer.js
Paste the following code inside the newly created layer.js
const { matchPath } = require("../lib/helpers");
class Layer {
/*
Setting up path and handler
*/
constructor(path, handler) {
this.handler = handler;
this.name = handler.name || "<anonymous>";
this.path = path;
}
/*
If the current request path matches the layer's path
then handling for the current path
*/
requestHandler(...args) {
const handler = this.handler;
handler ? handler(...args) : null;
}
/*
To match current request path with
the path provided at the time of setup
SETUP: app.get('/login', (req, res) => {})
CURRENT REQUEST: GET /login
*/
match(path) {
return matchPath(this.path, path);
}
}
module.exports = Layer;
Inside the router folder, create a route.js and paste the following code
touch route.js
const Layer = require("./layer.js");
class Route {
constructor(path) {
this.path = path;
this.stack = [];
this.methods = {};
}
requestHandler(method) {
const name = method.toLowerCase();
return Boolean(this.methods[name]);
}
get(handler) {
const layer = new Layer("/", handler);
layer.method = "get";
this.methods["get"] = true;
this.stack.push(layer);
return this;
}
post(handler) {
const layer = new Layer("/", handler);
layer.method = "post";
this.methods["post"] = true;
this.stack.push(layer);
return this;
}
dispatch(req, res) {
const method = req.method.toLowerCase();
this.stack.forEach((item) => {
if (method === item.method) {
item.requestHandler(req, res);
}
});
}
}
module.exports = Route;
Let's understand the above code snippet
- We created a
Routeclass that takespathas a parameter and maintains an internal array of all layers and a collection of methods attached to it. - Whenever, you do something like
app.get("/login", (req, res) => {
/* some processing */
});
Then a route is created with path /login and a layer is pushed inside its array of layers containing the handler provided with it. Route also stores the information about the current method in a collection. It is done to determine if a method of handling is available when a request comes in.
-
dispatchmethod is called when a request comes in, it traverses all the layers and if the current request method matches with the layer's method then we execute the apt request handler. -
Currently, we are handling two HTTP verbs,
getandpost.
Now, we have our Layer and Route components ready. Let's consolidate all this in Router.
Create a file index.js in the router folder and paste the following code.
touch router.js
const Layer = require("./layer.js");
const Route = require("./route.js");
class Router {
constructor() {
this.stack = [
new Layer("*", (req, res) => {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain");
res.end(`Cannot find ${req.url}`);
}),
];
}
handle(req, res) {
const method = req.method;
let found = false;
this.stack.some((item, index) => {
if (index === 0) {
return false;
}
const { matched = false, params = {} } = item.match(req.pathname);
if (matched && item.route && item.route.requestHandler(method)) {
found = true;
req.params = params;
return item.requestHandler(req, res);
}
});
return found ? null : this.stack[0].requestHandler(req, res);
}
route(path) {
const route = new Route(path);
const layer = new Layer(path, (req, res) => route.dispatch(req, res));
layer.route = route;
this.stack.push(layer);
return route;
}
get(path, handler) {
const route = this.route(path);
route.get(handler);
return this;
}
post(path, handler) {
const route = this.route(path);
route.post(handler);
return this;
}
}
module.exports = Router;
- We created a Router class that contains a stack (array of Layers). We are seeding the array with a default layer which will act as the default response if no handler for a path is found.
- We are handling
GETandPOSTrequests for now. - Whenever we invoke any of the HTTP verb methods then a
Routeis created with the provided path, aLayeris created with the same path and the route's dispatch method is provided as the handler to theLayer. handlemethod goes through each layer in the stack looking for the layer whose setup path matches with the current request path and if that layer has a route and route handler with it then route handler is executed.
Updating Framework
Let's update our framework to accommodate our routing system.
...
const Router = require('./router/index');
...
function Minimal() {
...
const _router = new Router();
...
function findNext(req, res) {
...
const next = () => {
...
if (matched) {
req.params = params;
middleware.handler(req, res, next);
} else if (current <= _middlewares.length) {
next();
} else {
req.handler(req, res);
}
};
...
}
function handle(req, res, cb) {
...
req.handler = cb;
...
}
function get(...args) {
const { path, handler } = checkMiddlewareInputs(args);
return _router.get(path, handler);
}
function post(...args) {
const { path, handler } = checkMiddlewareInputs(args);
return _router.post(path, handler);
}
function listen(port = 8080, cb) {
return http
.createServer((req, res) => {
...
handle(req, res, () => _router.handle(req, res));
})
...
}
return {
use,
listen,
get,
post
}
}
- We imported our
Routerclass into our framework and created an instance of it. - We implemented and exposed
getandpostmethods so that our users can use them to bind handlers to a specific path and method types. - We are now sending a
callbackto thehandlemethod which is assigned to a keyhandleron the request object. - We have seen in part 2 that we initially execute all the middlewares sequentially. We have updated that to call our method request handler when the middleware execution list is exhausted as no more middleware is left to be executed.
Testing
Now, we can test what we have built so far. Open server.js and make the following changes.
..
app.use(cors());
app.get('/about', (req, res) => {
res.send('I am the about page');
});
app.get('/', (req, res) => {
fs.readFile(path.resolve(__dirname, 'public', 'index.html'), (err, data) => {
if (err) {
return res.status(500).send('Error Occured');
}
return res.status(200).send(data);
});
});
...
/aboutroute withCORSheaders.

/route.

- Not Found Route

Yaay!! Our framework is working now. I know we haven't tested the post requests and there are some known issues. Let's see if you can find those out. In the next part, we will improve upon our current system, fix some issues and maybe build a simple REST API using our framework. Stay tuned.
Complete code for this part can be found here.




