A Better Way to Register Routes in Express

By

A Quick Overview Before Diving In

Express is a web application framework built for for Node.js. It was designed for building web applications and APIs; some say it’s the de-facto standard server framework available today.

If you’ve never used Express or aren’t familiar with how it works, I recommend checking out the official docs before diving into this post.

Simple Routing in Express

There are many ways to create routes in Express. Before we jump into creating routes, lets quickly set up our server.

The first thing I’m going to do is create a file called app.js in our root directory. This will be the entry point of our application. It will be responsible for starting up the server, registering routes, setting up our database (if we need one, which we don’t), etc.

const express = require('express');
const app = express();

let port = process.env.PORT || 3000;
app.listen(port);

console.log("Server started on port:", port);

And that’s all there is to it. We started up our server in less than 5 lines of code. Mind you, our server isn’t actually doing anything besides listening on the port that we specified. Let’s change that by creating our first route.

We can create our first route with a few lines of code. Let’s define a GET method to the root of our app.

app.get('/', function (req, res) {
  res.send('Hello GET Request!');
});

We can also just as easily create a POST method as well.

app.post('/', function (req, res) {
  res.send('Hello POST Request!');
});

By using app.route() we can clean things up and remove some redundancy by chaining both methods on the same route.

app.route('/')
    .get(function(req, res) {
        res.send('Hello GET Request');
    })
    .post(function(req, res) {
        res.send('Hello POST Request');
    });

With these two routes in place, if we start our server and open our browser to localhost:3000 we should see the message ‘Hello GET Request!’. Likewise, if we make a POST request (curl -X POST http://localhost:3000) we will be returned the message ‘Hello POST Request!’.

The paths specified for routes do not need to be literal strings. We could also use string patterns and regular expressions. Routes can also accept parameters. You can read more about the basics of routing here.

I usually find that as an application gets bigger, so does the number of routes. Even with the ability to chain routes using app.route(), creating all of your routes in one file can get very messy, very quickly. One way to avoid this is by using express.Router.

Express Router

The express.Router allows us to create more modular routes, by splitting up our routes into separate files.

Here’s a quick example of how the Express Router works. Let’s create two separate files, auth.js and user.js. In both files we attach routes to express.Router. We then expose (i.e. export) these routes and register them to our application in app.js.

// auth.js

var express = require('express');
var router = express.Router();

router.post('/login', function(req, res) {
    // Do something cool in here
});

module.exports = router;
// user.js

var express = require('express');
var router = express.Router();

router.get('/details/:userId', function(req, res) {
    // Do something cool in here
});

module.exports = router;
// app.js

const express = require('express');
const app = express();
const authRoutes = require('./auth');
const userRoutes = require('./user');

// Register our routes
app.use('/auth', authRoutes);
app.use('/user', userRoutes);

let port = process.env.PORT || 3000;
app.listen(port);

console.log("Server started on port:", port);

That’s a lot cleaner, right? In the above example we used express.Router to create two separate routes (/auth/login and /user/details/:id). The code in app.js is much cleaner, and provides better flexibility for adding more routes as our application scales.

With that being said, we can still do better!

Dynamically Registering Routes

I was recently tasked with setting up a fairly large web application in Node. I wanted to set up the routes in such a way that:

  1. Developers should be able to add new routes with very minimal effort.
  2. The developer should be focused on the logic of the route itself, not on where or how to register it to our application.
  3. All routes were kept organized, modularized and in one place.

I created a directory called routes where all of the the application’s routes live. The routes would be registered based on the directory structure. For example, this directory structure …

|- routes
    |- auth
        |- login.js
        |- sign-up.js
    |- user
        |- list.js
        |- details
            |- profile.js
    |- settings
        |- list.js

… will generate the following routes:

/auth/login
/auth/sign-up
/user/list
/user/details/profile
/settings/list

All of the routes in the application would be created and registered once at start-up. For each route, I needed to know which methods it supports (i.e. GET, POST, etc). As an example, here’s how I would define the auth/login route in login.js.

// login.js

module.exports = {
    post: function(req, res) {
        res.json({
            success: true
        });
    },

    get: function(req, res) {
        res.json({
            success: true
        });
    }
};

Each route file exposes a simple Object where the key is the request method type and the corresponding value is the route itself. All of the routes are defined the exact same way.

When the application boots up, I needed to loop through the routes directory and dynamically create them on the fly. This would allow other developers working on this application to simply add their route to the routes directory, without having to worry about manually naming the endpoint URL or registering it to the application. It also keeps the logic in app.js super clean and easy to follow.

I created a Server class which was responsible for starting up my server, registering middleware and registering all of the routes in the routes directory.

// Server.js

class Server {
    constructor() {}

    start() {
        this.registerMiddleware();
        this.registerRoutes();

        // Start listening
        let port = process.env.PORT || 3000;
        app.listen(port);

        console.log("Server started on port:", port);
    }

    registerMiddleware() {
        // Ignore this for now, but register all middleware in here ...
    }

    registerRoutes() {
        // Register all of the routes defined in the '/routes' directory
    }
}

module.exports = Server;
// app.js

const Server = require('./lib/Server');

// Lets get jiggy with it
let server = new Server();
server.start();

Okay! As you can see, I moved most of the logic from app.js into server.js. I’m going to ignore the registerMiddleware function (which is pretty self-explanatory), and jump straight into registerRoutes.

The first thing I needed to do was traverse through all of the files and directories in the routes directory. If I came across a file, I needed to register it as a route. If I came across a directory, I needed to recursively loop through all of it’s files/directories and continue to register routes until all routes were handled.

The last step was to actually register the route itself. As I mentioned above, each route was defined as an Object where the key represented the method types and the corresponding value represented the route itself. In order to do that, I’d needed to import the route, loop through each key (i.e. method) and register each one as a route. The URL for each route would be defined by the actual path of the route file itself.

Here’s the finished product:

registerRoutes() {
    const self = this;
        const BASE_DIR = process.cwd() + '/routes';

        // Get all of the files in the specified directory
        let files = fs.readdirSync(BASE_DIR + dir);

        // Traverse through the files/directories
        for (let path of files) {
            let filePath = BASE_DIR + dir + '/' + path;
            let stats = fs.lstatSync(filePath);

            // If file, then register route
            if (stats.isFile()) {
                console.info("Registering routes: ", dir);

                let endpoint = require(filePath);

                // Loop through all method types of our endpoint and register
                // them to the app.
                Object
                    .keys(endpoint)
                    .forEach((method) => {

                        // Create the endpoint URL
                        let endpointUrl = dir + '/' + path.replace('.js', '');
                        console.info("\t Creating route [%s]: %s", method, endpointUrl);

                        // Register the route
                        app[method](endpointUrl, endpoint[method]);
                    });
            }

            // If directory, then loop through this directory and register routes
            if (stats.isDirectory()) {
                self._registerRoutes(dir + '/' + path);
            }
        }
}

Pretty cool right?

A few things to note here. As you can see I define my routes the exact same way as above:

app[method](endpointUrl, endpoint[method]);

Where method is the key (i.e. ‘get’, ‘post’, ‘put’, etc.), endpointUrl is the path of the file with the extension stripped (i.e. /auth/login) and endpoint[method] is the actual route itself.

As I mentioned above, Express allows you to include patterns, regular expressions and parameters in the route URL. You may have noticed that my example above does not currently support these features (which is probably something you’ll want to have). Not to worry though, with just a few modifications to your routes definition and to the registerRoutes function, you’ll be good to go!

For example, I can set up my route definitions like so:

// user/details.js

module.exports = {
    url: '/user/details/:userId',
    routes: {
        get: function(req, res) {
            // GET request
        },

        post: function(req, res) {
            // POST request
        }
    }
};

I can now set endpointUrl in the registerRoutes function to use url if it is provided, and default to the path name. This may not be the best way to go about it, but it’s just one of many ways.

I hope you found this helpful! As always if you have any question or comments please leave them below.