Set up a smarter routing system with Node.js and Express

At FusionWorks we enjoy using the NestJS framework, which helps a lot to keep our Node.js code clean and well-structured thanks to its modular architecture. But what if you only have Express at a hand? Could we achieve something similar?

Image generated by MidJourney AI for “developer creates smart routing system using Express”

In this tutorial, we’ll set up routes with “dummy” handler functions. On completion, we’ll have a modular structure for our route handling code, which we can extend with real handler functions. We’ll also have a really good understanding of how to create modular routes using Express!

Let’s say we have three routes (//home/about), each one with two different HTTP verbs (getpost).

Our goal here is to create a separate file for each path and make use of the Express Router object. The Router object is a collection of middleware and routes. It is a mini-app within the main app. It can only perform middleware and routing functions and can’t stand on its own.

// routes/root.js
const express = require("express");
const router = express.Router();
router
.route("/")
.get((req, res) => res.send("getting /"))
.post((req, res) => res.send("posting /"));

module.exports = router;// routes/home.js
const express = require("express");
const router = express.Router();
router
.route("/home")
.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("/about")
.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}`)
);

By having each route in its own file, we’re achieving a less messy code in the index.js file. The problem that still persists here — is that every time we add a new route file, the main file has to change as well, in order to map the path to the file.

Achieving a greater number of routes — produces the same problem: the main file gets bigger and messier.

We can solve this issue by creating a separate file that maps all the other routes and making use of it inside 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;
const bootstrapRoutes = require("./routes");
bootstrapRoutes(app);
app.listen(port, () =>
console.log(`App listening at http://localhost:${port}`)
);

The routes folder index file receives the app instance from the main file and makes the path mapping. 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.

This can be improved if we would loop through the routes folder’s files and map them accordingly. We’ll be using the filesystem readdirSync method. This method is used to synchronously read the contents of a given directory,
it also returns an array with all the file names or objects in the directory.

// routes/index.js
const snakeCase = require("lodash/snakeCase");
const express = require("express");
const fs = require("fs");
const path = require("path");module.exports = (app) => {
const files = fs.readdirSync(__dirname);
files.forEach(file => {
if (file === "index.js") {
return;
} const filePath =
file !== "root.js"
? file.replace(".js", "")
: "";

const router = express.Router(); const currentRoute =
require(path.join(__dirname, `/${filePath}`))(router); app.use(`/${filePath}`, currentRoute);
});
}

References