DevTools.tech

Build your own expressjs | Part 3

April 07, 2019

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 checkout part 1 here and part 2 here.

We all 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.

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 checkout 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:

  1. Router
  2. Layer
  3. Route

Let’s talk about each but before that take a look at the diagram below.

Routing

Router

Router component is responsible for 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.

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.

Difference between the Layer in Router component and Route component is that in Route component, a Layer contains a method and its handler.

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

route

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 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 current request path matches the layer's path
		then handling for current path
	*/
	requestHandler(...args) {
		const handler = this.handler;
		handler ? handler(...args) : null;
	}

	/*
		To match current request path with 
		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 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

  1. We created a Route class which takes path as parameter and maintains an internal array of all layers and collection of methods attached to it.
  2. 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 so in order to determine if a method handling is available when a request comes in.

  1. dispatch method is called when a request comes in, it traverses all the layers and if current request method matches with layer’s method then we execute the apt request handler.

  2. Currently, we are handling two HTTP verbs, get and post.

Now, we have our Layer and Route components ready. Let’s consolidate all this in Router.

Create a file index.js in 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;
  1. We created a Router class which contains a stack (array of Layers). We are seeding the array with a default layer which will act as default response if no handler for a path is found.
  2. We are handling GET and POST requests for now.
  3. Whenever, we invoke any of the HTTP verb method then a Route is created with the provided path, a Layer is created with same path and route’s dispatch method is provided as handler to the Layer.
  4. handle method goes through each layer in the stack looking for the layer whose setup path matches with 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 accomdate 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	}
}
  1. We imported our Router class in our framework and created an instance of it.
  2. We implemented and exposed get and post methods so that our users can use it to bind handlers to specific path and method types.
  3. We are now sending a callback to handle method which is assigned to a key handler on request object.
  4. We have seen in the 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 in 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);	});
});
...
  1. /about route with CORS headers.

about

  1. / route.

home

  1. Not Found Route

not-found

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 may be build a simple REST api using our framework. Stay tuned.

Complete code for this part can be found here.

Hopefully, this article helped you in some way and if yes, then kindly tweet about it by clicking here Twitter Logo. Feel free to share your feedback here.


Yomesh Gupta

Hi, I am Yomesh Gupta. I am trying to find a perfect blend of design and technology! This is my blog where I write about things which fascinate me. Let me know your views here.

Newsletter.

Subscribe to get notified about new content. No spam ever!