(Do-It-Yourself: Node HTTP Router) Express is a terrific JavaScript framework that serves as the backend for a lot of full stack web applications. Many of us use it day-to-day and are proficient in how to use it but may lack an understanding of how it works. Today, without diving into the Express source code, we’re going to recreate some of the routing functionality to gain a better understanding of the the context in which the framework operates as well as how response and request can be handled. If you’d like to see the final source code, you can find it on . Please do still code along with me for a better learning experience! Github here Getting Started Let’s start out by emulating Express’ . We’ll modify it slightly since we won’t be pulling in but will rather be pulling in a module we create ourselves. “Hello World” application express First, create a new project folder and initiate an npm project using the default configuration. mkdir diy- cd diy- npm init -y node -router node -router Verify your file looks as follows: package.json { : , : , : , : , : { : }, : , : } "name" "diy-node-router" "version" "1.0.0" "description" "" "main" "index.js" "scripts" "test" "echo \"Error: no test specified\" && exit 1" "author" "" "license" "ISC" Next, we’ll create our file. In this file we’ll replicate the “Hello World” example but pull in our own module (we’ll create this module in short order). index.js express router = ( ); app = router(); port = ; app.get( , (req, res) => res.send( )); app.listen(port, () => .log( )); const require './src/diy-router' const const 3000 '/' 'Hello World!' console `Example app listening on port !` ${port} This is essentially the same as the “Hello World” example example. Based on this code, we know our module should be a function that returns an object when called. This object should have a method to start listening for requests on a port and a method to set up request handling. We’ll also set up a method since we’ll ultimately want our app to handle posts. express router app listen get get post Scaffolding the diy-router Module Now we create the actual router module. Create the file inside a new directory. diy-router.js src mkdir src cd src touch diy-router.js We don’t want to bite off too much at once, so let’s first just create a module that exports the requisite methods. .exports = { router = { get = { .log( ); }; listen = { .log( ); }; { get, listen }; }; router; })(); module ( ) => ( const => () const ( ) => route, handler console 'Get method called!' const ( ) => port, cb console 'Listen method called!' return return Hopefully this all makes sense so far: we created a function that, when called, returns a and a method. At this point, each method ignores its parameters and simply logs that it has been called. This function is then wrapped in an Immediately Invoked Function Expression (IIFE). If you’re unfamiliar as to why we use an IIFE, we do so for data privacy. This will be a little more obvious is the coming steps when we have variables and functions that we don’t want to expose outside the module itself. router get listen At this point, we can go back to our root directory and run our application using node. node . If all is well, you’ll see an output like the following: Get ! ! method called Listen method called Perfect, everything is wired together! Now, let’s start serving content in response to http requests. Handling HTTP Requests To get some basic HTTP request handling functionality, we bring in node’s built-in module to our . The module has a method that takes a function with request and response parameters. This function gets executed every time an http request is sent to the port specified in the method. The sample code below shows how the module can be used to return the text “Hello World” on port . http diy-router http createServer listen http 8080 http.createServer( { res.write( ); res.end(); }).listen( ); ( ) => req, res 'Hello World!' 8080 We’ll want to use this kind of functionality it our module, but we need to let the user specify their own port. Additionally, we’ll want to execute a user-supplied callback function. Let’s use this example functionality along with within the method of our module and make sure to be more flexible with the port and callback function. listen diy-router http = ( ); .exports = { router = { get = { .log( ); }; listen = { http.createServer( { res.write( ); res.end(); }).listen(port, cb); }; { get, listen }; }; router; })(); const require 'http' module ( ) => ( const => () const ( ) => route, handler console 'Get method called!' const ( ) => port, cb ( ) => req, res 'Hello World!' return return Let’s run our app and see what happens. node . We see the following logged in the console: method called! Example app listening on 3000! Get port This is a good sign. Let’s open our favorite web browser and navigate to . http://localhost:3000 (Simple “Hello World!” App) Looking good! We’re now serving content over port 3000. This is great, but we’re still not serving route-dependent content. For example, if you navigate to you’ll see the same “Hello World!” message. In any real-world application, we’ll want the content we serve to our user to be dependent on what’s in the provided URL. http://localhost:3000/test-route Adding and Finding Routes We need to be able to add any number of to our application and execute the correct route handler function when that route is called. To do this, we’ll add a routes array to our module. Additionally, we’ll create and functions. Notionally, the code might look something like this: routes addRoute findRoute routes = []; addRoute = { routes.push({ method, url, handler }); }; findRoute = { routes.find( route.method === method && route.url === url); } let const ( ) => method, url, handler const ( ) => method, url return => route We’ll use the method from our and methods. The method simply returns the first element in that matches the provided and . addRoute get post findRoute routes method url In the following snippet, we add the array and two functions. Additionally, we modify our method and add a method, both of which use the function to add user-specified routes to the routes array. get post addRoute Since the array and the and methods will only be accessed within the module, we can use our IIFE “revealing module” pattern to not expose them outside the module. Note: routes addRoute findRoute http = ( ); .exports = { routes = []; addRoute = { routes.push({ method, url, handler }); }; findRoute = { routes.find( route.method === method && route.url === url); } router = { get = addRoute( , route, handler); post = addRoute( , route, handler); listen = { http.createServer( { res.write( ); res.end(); }).listen(port, cb); }; { get, post, listen }; }; router; })(); const require 'http' module ( ) => ( let const ( ) => method, url, handler const ( ) => method, url return => route const => () const ( ) => route, handler 'get' const ( ) => route, handler 'post' const ( ) => port, cb ( ) => req, res 'Hello World!' return return Finally, let’s employ the function within the function we’re passing to our method. When a route is successfully found, we should call the handler function associated with it. If the route isn’t found, we should return a 404 error stating that the route wasn’t found. This code will notionally look like the following: findRoute createServer method = req.method.toLowerCase(); url = req.url.toLowerCase(); found = findRoute(method, url); (found) { found.handler(req, res); } res.writeHead( , { : }); res.end( ); const const const if return 404 'Content-Type' 'text/plain' 'Route not found.' Now let’s incorporate this into our our module. While we’re at it, we’ll add one extra bit of code that creates a method for our response object. send http = ( ); .exports = { routes = []; addRoute = { routes.push({ method, url, handler }); }; findRoute = { routes.find( route.method === method && route.url === url); } router = { get = addRoute( , route, handler); post = addRoute( , route, handler); listen = { http.createServer( { method = req.method.toLowerCase(); url = req.url.toLowerCase(); found = findRoute(method, url); (found) { res.send = { res.writeHead( , { : }); res.end(content); }; found.handler(req, res); } res.writeHead( , { : }); res.end( ); }).listen(port, cb); }; { get, post, listen }; }; router; })(); const require 'http' module ( ) => ( let const ( ) => method, url, handler const ( ) => method, url return => route const => () const ( ) => route, handler 'get' const ( ) => route, handler 'post' const ( ) => port, cb ( ) => req, res const const const if => content 200 'Content-Type' 'text/plain' return 404 'Content-Type' 'text/plain' 'Route not found.' return return Let’s see this in action! Again, run your application from the root directory. node . You should see that the app is being served on port 3000. In your browser, navigate to . You should see “Hello World!” But now, if you navigate to , you should get a “Route not found” message. Success! http://localhost:3000 http://localhost:3000/test-route Now we want to confirm we can actually add as a route in our application. In , set up this route. /test-route index.js router = ( ); app = router(); port = ; app.get( , (req, res) => res.send( )); app.get( , (req, res) => res.send( )); app.listen(port, () => .log( )); const require './src/diy-router' const const 3000 '/' 'Hello World!' '/test-route' 'Testing testing' console `Example app listening on port !` ${port} Restart the server and navigate to . If you see “Testing testing”, you’ve successfully set up routing! http://localhost:3000/test-route If you’ve had enough fun, you can end here! This was a great primer on routing. If you want to dig a little deeper and be able to extract parameters from ourroutes, read on! Note: Extracting Route Parameters In the real world, we’re likely to have parameters in our url strings. For example, say we have a group of users and want to fetch a user based on a parameter in the url string. Our url string might end up being something like where represents a unique identified associated with a user. /user/:username username To create this function, we could develop some regular expression rules to match any url parameters. Instead of doing this, I’m going to recommend we pull in a great module called to do this for us. The module creates a new object for each route that has a method with all the regular expression magic baked in. To make the required changes in our module, do the following: route-parser route-parser match Install the module from the command line: npm route-parser i At the top of the file, require the module. diy-router.js = require( ); const Route 'route-parser' In the function, rather than adding the plan url string, add a new instance of the class. addRoute Route addRoute = { routes.push({ method, : Route(url), handler }); }; const ( ) => method, url, handler url new Next, we’ll update the function. In this update, we use the object’s method to match the provided url with a route string. In other words, navigating to will match the route string . findRoute Route match /user/johndoe /user/:username If we do find a match, we don’t only want to return a match, but we’ll also want to return the parameters extracted from the url. findRoute = { route = routes.find( { route.method === method && route.url.match(url); }); (!route) ; { : route.handler, : route.url.match(url) }; }; const ( ) => method, url const => route return if return null return handler params To handle this new functionality, we need to revisit where we call in the function we pass to . We’ll want to make sure that any parameters in our route get added as a property on the request object. findRoute http.createServer (found) { req.params = found.params; res.send = { res.writeHead( , { : }); res.end(content); }; if => content 200 'Content-Type' 'text/plain' So our final module will look like this: http = ( ); Route = ( ); .exports = { routes = []; addRoute = { routes.push({ method, : Route(url), handler }); }; findRoute = { route = routes.find( { route.method === method && route.url.match(url); }); (!route) ; { : route.handler, : route.url.match(url) }; }; get = addRoute( , route, handler); post = addRoute( , route, handler); router = { listen = { http .createServer( { method = req.method.toLowerCase(); url = req.url.toLowerCase(); found = findRoute(method, url); (found) { req.params = found.params; res.send = { res.writeHead( , { : }); res.end(content); }; found.handler(req, res); } res.writeHead( , { : }); res.end( ); }) .listen(port, cb); }; { get, post, listen }; }; router; })(); const require 'http' const require 'route-parser' module ( ) => ( let const ( ) => method, url, handler url new const ( ) => method, url const => route return if return null return handler params const ( ) => route, handler 'get' const ( ) => route, handler 'post' const => () const ( ) => port, cb ( ) => req, res const const const if => content 200 'Content-Type' 'text/plain' return 404 'Content-Type' 'text/plain' 'Route not found.' return return Let’s test this out! In our file, we’ll add a new user endpoint and see if we can toggle between users by changing our url query string. Change you file as follows. This will filter our array based on the property of the provided request. index.js index.js user params router = ( ); app = router(); port = ; app.get( , (req, res) => res.send( )); app.get( , (req, res) => res.send( )); app.get( , (req, res) => { users = [ { : , : }, { : , : } ]; user = users.find( user.username === req.params.username); res.send( ); }); app.listen(port, () => .log( )); const require './src/diy-router' const const 3000 '/' 'Hello World!' '/test-route' 'Testing testing' '/user/:username' const username 'johndoe' name 'John Doe' username 'janesmith' name 'Jane Smith' const => user `Hello, !` ${user.name} console `Example app listening on port !` ${port} Now, restart your app. node . Navigate first to , observe the content, and then navigate to . You should receive the following responses, respectively: http://localhost:3000/user/johndoe http://localhost:3000/user/janesmith John Doe! Hello, Jane Smith! Hello, Final Code The final code for this project can be found on . Thanks for coding along! Github here Conclusion In this article we observed that, while Express is an incredible tool, we can replicate its routing functionality through implementation of our own custom module. Going through this kind of exercise really helps to pull back the “curtain” and makes you realize that there really isn’t any “magic” going on. That being said, I definitely wouldn’t suggest rolling your own framework for you next Node project! One reason frameworks like Express are so incredible is they have received a lot of attention from a lot of terrific developers. They have robust designs and tend to be more efficient and secure than solutions any single developer could deploy. So what did you think of this exercise? Does this inspire you to try to replicate other aspects of Express’ functionality? Let me know in the comments!