Let’s build a NodeJS micro framework (Part II)

Learning to build an express like NodeJS framework …

In today’s post, we are going to implement the router class of our framework. We have already discussed the directory structure of the framework in Part I of this series. The router component will be implemented in src/router.js.

Our router will be implemented as a regular expression matcher behind the scene while providing a higher level of route specification syntax. We have already seen some examples of adding routes to an application using the framework. An important aspect of routing we won’t implement in Micro is URL generation from routes. This should be an exercise for active readers.

So without further ado, let me show you the skeleton of our router class in its most glorified form 😝.

The listing above shows the initialization of the _routes, _prefix member variables of the router class to an array and empty string respectively. Perhaps the most important method is the one named route. As you can see this method accepts three arguments:

Our objective in this method is to build a route object and add it to the _routes array member variable. In order to put in context the explanation, let’s suppose we call the route method as follows:

    router.route(‘/:class/students/:id/:session?’, [‘GET’, ‘POST’], [ _ => true, _ => false])

Our _routes member variable should end up with the following content:

    [{ 
        methods: [ 'GET', 'POST' ],
        regex: /^\/([_a-zA-Z0-9\-]+)\/students\/([_a-zA-Z0-9\-]+)(\/([_a-zA-Z0-9\-]+))?$/,
        params: [ 'class', 'id', 'session' ],
        handlers: [ [Function], [Function] ] }
    }]

Please notice how the route object contains all the information we need. The methods, handlers properties are easy to map, because they are just direct assignment from the actual parameters received from the call to router.route(...). In order to understand the remaining properties, let’s implement the route and group methods of the router class.

In the route method, we first check if _prefix is not empty. We tweak the specs parameter accordingly by prefixing it. Then we call _cleanHandlers helper method to validate the request handlers making sure they are callable. This helper method also converts single handler into an array of handlers. Next, we push into the _routes member variable the newly created route object. The extraction of the params and regex properties of the route happens in the call and spread operation …this._patternToRegex(specs). Before looking into the implementation of this helper method though, let’s wrap up our discussion on the remaining methods above.

The group method accepts a prefix and a callback function, It assigns the prefix value to _prefix and invokes the callback passing the instance of the router. The _prefix is reset to an empty string after the callback invocation. This technique is a way of maintaining the router prefix while the routes are being created in the callback. Consequently, any route created in the callback will end up with the current value of _prefix.

The all method is one of the many router API methods that allow users of the framework to add routes without listing the HTTP methods manually. The remaining methods you will find in the repository on Github are: get, post, put and delete.

Now that all these noisy methods are out of the way, let’s focus on the most important methods of our router class.

The _patternToRegex(specs), converts a route specification to a regular expression. I should have named this _specToRegex, I was just not willing to refactor this time. It’s just a toy project after all 😃. Let’s not waste time on this matter and show the implementation.

In this method, we first initialize the regex and params to an empty string and empty array respectively. Next, we break the specification into parts and build each part as a small chunk of the regular expression that will be generated. Looping through the parts, we first skip any empty part. This takes into account trailing forward slashes. Next, we check if this part is a route parameter (e.i does it starts with a colon). If yes, we construct a simple regular expression that accepts characters allowed in a URL while taking into account the optional specifier ?. Otherwise, we just add the part to the regular expression. Please note how we add the name of the route parameter by cleaning it and pushing into the params array. Finally, we create a Javascript RegExp object from our generated regex string and return an object made of: the regular expression object and the route parameter names array.

The last bit we need to tackle in this Router class is the _dispatch method. Recalling from Part I, this is the method in charge of finding the appropriate handler for processing an incoming HTTP request. This method might seem a bit tricky. Let’s first show the implementation and then explain how it works.

In this method, we will receive the NodeJS HTTP server request and response object as parameters. Recall from Part I that we patched the response with a render method for the purpose of template rendering. In the above listing, we essentially do three things:

First, we try to find a matching route based on the URL property of the request. If no match is found, we simply reply with a 404 status. Next, If a match is found, we try to extract all the route params value from the matching URL and map them to the params name. We use the regular expression captured groups to ignore those with value undefined or value starting with a slash. We then loop through params names to build a key/value pair between the names and the values that we finally assign to request.params. This makes our second act of patching an object from the NodeJS core library🕺. Finally, we need to build a chain of handlers out of the matched route handlers array. Again let’s recall this snippet taken from Part I.

For the next handler to be invoked, The developer needs to explicitly call the next argument received in the route handler. To achieve this, we need to embed the handlers inside each other and end up with the first handler being the top-level handler. Basically, we want to go from a flat array of handlers to a Russian dolls {:target=”_blank”} like data structure of handlers. We could implement this with a Stack or a Linked List. However, it feels more natural to explain this using the Russian dolls approach.

Since we need to start building our handler chain from the last handler, we copy and reverse the handlers from the matched route const handlerStack = route.handlers.slice().reverse(). It’s very important to make a copy, because this chain is built specifically for the current HTTP request being processed. This chain will embed data (request, response) specific to the current HTTP request.

To make things easier, we adopt a bottom-up approach. We first build the last handler which will be embedded into the first to last handler which in turn will… and so on. We loop through handlerStack: In the first iteration, nextHandler is undefined. This makes sense because, inside the last handler, the next parameter should be undefined as there is no subsequent handler in the chain. In the loop, we first declare lastNext and assigns it the value of nextHandler. After that, we need to encapsulate the current handler in the form of next #L28. Notice how we are passing all the current request data we need to handler(request, response, lastNext, …params) as well as the previously constructed next handler stored in lastNext. We also remember to reset nextHandler via this assignment nextHandler = nextafterward.

Once outside of the loop, nextHandler is a function pointing to the root of the handler chain. In other words, this is the biggest Russian doll that encapsulates the remaining once. The only thing left to do here is to invoke that top-level handler and mark the request as successfully handled handledRequest = true.

Notes: In a real world scenario, just imagine how you would arrange the Russian dolls in your backpack. Packing is the process of building the handler chain, Unpacking is the process of calling the top level handler.

This previous section concludes the implementation details of our Router component. Ultimately, a production-grade router will require more validation, as well as URL generation from named routes. However, our implementation already has the core features of any production-grade HTTP router component.

Congratulation for making it through 👏 👏 👏. I reckon that implementing the handler chain gets a bit difficult because of the concepts comming into play. Please review the following:

Next time, we will explore and implement the template engine component of our framework. Until then, Happy coding! 👋🏽