How to Write Smarter Routing With Express and Node.js

How to Write Smarter Routing With Express and Node.js

  • 192

Express is a web application framework for Node. It provides various features that make web-application development fast and easy, a task that otherwise takes more time when using only Node

Express is a web application framework for Node. It provides various features that make web-application development fast and easy, a task that otherwise takes more time when using only Node.

There are many ways you can create and manage routes using Express, the most simple and direct being through the use of the delete, get, post, and put methods. Each one of them is mapped to the equivalent request HTTP verb.

const express = require("express");
const app = express();
const port = 3000;
app.get("/", (req, res) => res.send("GET"));
app.post("/", (req, res) => res.send("POST"));
app.get("/home", (req, res) => res.send("GET HOME"));
app.post("/home", (req, res) => res.send("POST HOME"));
app.get("/about", (req, res) => res.send("GET HOME"));
app.post("/about", (req, res) => res.send("POST HOME"));
app.listen(port, () =>
    console.log(`App listening at http://localhost:${port}`)
);

article_express_routing_medium_1.js

In the example above, we have three routes (/, /home, /about), each one with two different HTTP verbs (get, post) and its own logic that’s sending a unique text to the client

As you can see from this tiny example, this kind of approach can get very messy when the number of routes increase.

There’s a way we can make this code better by using the [route](https://expressjs.com/en/4x/api.html#app.route) method and method chaining to equal paths:

const express = require("express");
const app = express();
const port = 3000;
app.route("/")
    .get((req, res) => res.send("GET"))
    .post((req, res) => res.send("POST"));
app.route("/home")
    .get((req, res) => res.send("GET HOME"))
    .post((req, res) => res.send("POST HOME"));
app.route("/about")
    .get((req, res) => res.send("GET HOME"))
    .post((req, res) => res.send("POST HOME"));
app.listen(port, () =>
    console.log(`App listening at http://localhost:${port}`)
);

article_express_routing_medium_2.js
This way we can make the code a little bit shorter, but it’s still messy since we have multiple route paths in the same file.

Our next step is to create a separate file for each path and make use of the Express [Router](https://expressjs.com/en/4x/api.html#router) object. The Router object is an isolated instance of middleware and routes. You can think of it as a mini-application, capable only of performing middleware and routing functions.

// /routes/root.js
const express = require("express");
const router = express.Router();
router
    .route("/")
    .get((req, res) => res.send("GET"))
    .post((req, res) => res.send("POST"));
module.exports = router;

// /routes/home.js
const express = require("express");
const router = express.Router();
router
    .route("/")
    .get((req, res) => res.send("GET HOME"))
    .post((req, res) => res.send("POST HOME"));
module.exports = router;

// /routes/about.js
const express = require("express");
const router = express.Router();
router
    .route("/")
    .get((req, res) => res.send("GET ABOUT"))
    .post((req, res) => res.send("POST ABOUT"));
module.exports = router;

// index.js

const express = require("express");
const app = express();
const port = 3000;
app.use("/", require("./routes/root"));
app.use("/home", require("./routes/home"));
app.use("/about", require("./routes/about"));
app.listen(port, () =>
    console.log(`App listening at http://localhost:${port}`)
);

article_express_routing_medium_3.js
This way, we have cleaner code in the main file, index.js, and each route is defined in its own file. In addition to the Route object, we make use of the Express method [use](https://expressjs.com/en/4x/api.html#app.use) to map the path to its configuration file.

It’s much better than what we had in the earlier examples, but I can still see some problems here — like the code repetition in each route file and, mainly, the fact that every time we add new route files to the app, we need to change the main file to map the path to the file. When we have a great number of routes, we have the same problem: The main file code gets bigger and messy.

To solve the second issue, we can make a separate file to map each path to its route file and do a simple require in the main file.

// /routes/index.js

module.exports = (app) => {
    app.use("/", require("./root"));
    app.use("/home", require("./home"));
    app.use("/about", require("./about"));
};

// index.js

const express = require("express");
const app = express();
const port = 3000;
require("./routes")(app);
app.listen(port, () =>
    console.log(`App listening at http://localhost:${port}`)
);

article_express_routing_medium_4.js
The routes folder index file receives the app instance from the main file and makes the path mapping. The main file makes use of the Immediately Invoked Function Expression to require and execute the routes index file passing the app created. Now we have a cleaner main file, but we still have the problem that it’s required to manually map each path to its file.

Node can help us make this better if we can make it loop through the routes folder’s files and make the mapping. To achieve this, we’ll make use of Node’s [fs.readdirSync](https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options). This method is used to synchronously read the contents of a given directory. The method returns an array with all the file names or objects in the directory.

And you may ask, what about the path? If we don’t have an automated way to discover the path of each route file, we’ll still have to edit the list for each route added. I can see two kinds of solutions for this issue: to use convention over configuration or to add a export with the path in the route file.

To use convention over configuration, we’ll use the filename as the route path. To make it safer and to remove potential problematic characters, we’ll use Lodash to convert the filename to snakeCase, where strings are separated by an underscore.

Another change we’ll make is to use the Router object only in the routes config file, making the route code simpler and avoiding code repetition.

// /routes/root.js
module.exports = (router) => {
    router
        .get("/", (req, res) => res.send("GET"))
        .post("/", (req, res) => res.send("POST"));
    return router;
};

// /routes/home.js
module.exports = (router) => {
    router
        .get("/", (req, res) => res.send("GET HOME"))
        .post("/", (req, res) => res.send("POST HOME"));
    return router;
};

// /routes/about.js
module.exports = (router) => {
    router
        .get("/", (req, res) => res.send("GET ABOUT"))
        .post("/", (req, res) => res.send("POST ABOUT"));
    return router;
};

// routes/index.js
const snakeCase = require("lodash/snakeCase");
const express = require("express");
module.exports = (app) => {
    require("fs")
        .readdirSync(__dirname)
        .forEach((file) => {
            if (file === "index.js") return;
            const path =
                "/" +
                (file !== "root.js" ? snakeCase(file.replace(".js", "")) : ""); // root.js file will map to /
            const router = express.Router();
            const route = require(require("path").join(__dirname, file));(router);
            app.use(path, route);
        });
};

// index.js
const express = require("express");
const app = express();
const port = 3000;
require("./routes")(app);
app.listen(port, () =>
    console.log(`App listening at http://localhost:${port}`)
);

article_express_routing_medium_5.js
The other option, as I said earlier, is to export the desired path name in the route file and use this information in the route config file.

To achieve this, we’ll have to export an object with the path and config keys in our route file. In this kind of configuration, I strongly suggest you use the filename path method as a fallback to avoid errors in case you forget to add the path export. This way, we have an extra bonus: a solution that’ll work on both situations.

In our final example, we’ll change the home route path to my_home and leave the about route file as it was.

// /routes/root.js
module.exports = {
    path: "/",
    config: (router) => {
        router
            .get("/", (req, res) => res.send("GET"))
            .post("/", (req, res) => res.send("POST"));
        return router;
    },
};


// /routes/home.js
module.exports = {
    path: "/my_home",
    config: (router) => {
        router
            .get("/", (req, res) => res.send("GET HOME"))
            .post("/", (req, res) => res.send("POST HOME"));
        return router;
    },
};

// /routes/about.js - here we dont export path to use the filename as a path
module.exports = (router) => {
    router
        .get("/", (req, res) => res.send("GET ABOUT"))
        .post("/", (req, res) => res.send("POST ABOUT"));
    return router;
};

// routes/index.js
const snakeCase = require("lodash/snakeCase");
const express = require("express");
module.exports = (app) => {
    require("fs")
        .readdirSync(__dirname)
        .forEach((file) => {
            if (file === "index.js") return;
            const router = express.Router();
            const routeModule = require(require("path").join(__dirname, file));
            const path =
                routeModule.path ||
                "/" +
                    (file !== "root.js"
                        ? snakeCase(file.replace(".js", ""))
                        : "");
            const route = routeModule.config
                ? routeModule.config(router)
                : routeModule(router);
            app.use(path, route);
        });
};

article_express_routing_medium_6.js
And that’s it. Here we have our smarter Express router config. By using this kind of configuration — and solutions like PM2 or Nodemon to watch your app folder and restart automatically on file changes — all you have to do to add a new route is to add a file in the routes folder.

Of course, there’s plenty of space for improvements, like error checking, but this kind of feature I’ll leave to you so you can get used to the code provided and practice your coding skills while trying to achieve whichever feature you think isn’t implemented in my example. To make things easier in your journey, I’ve provided the final code in CodeSandbox. Feel free to fork and work on it.

I hope you’ve learned something useful in this article. See you next time!