Skip to content

Route Organisation with Express 4+

Posted on:November 27, 2015

Caveat Emptor: As far as I can tell, this routing functionality will continue to work in Express 5, which at this time of writing is still in early alpha stage. Read the Express 5 Upgrading Guide here. Also, in the Nested Routes section below, I mention passing the mergeParams option the Router method - this was only introduced in Express 4.5.0.

The express.Router class was introduced in Express 4 to allow developers to instantiate routers as standalone modules with their own middleware and routing systems.

For me, the greatest benefit is that this affords more flexibility and makes it easier to manage different combinations of middleware of varying specificity.

This simple example gives a bird’s eye view of the different components of a working router implementation:

import express from "express";
//... other Express boilerplate
 
const mainRouter = express.Router();
mainRouter.get("woah", (req, res) => {
  //... do something
});
 
const userRouter = express.Router();
userRouter.use(authenticationMiddlware());
userRouter.get("profile", (req, res) => {
  //... do something
});
 
// mount the routers by specifying their mount points, in this case `/` and `/users`
app.use("/", mainRouter);
app.use("/users", userRouter);

At this point, the active routes would be:

GET /woah
GET /users/profile

How I Organise my Routes

Now, with a big app, we can split its many routes into different files, and instantiate/mount them neatly. This is great if you’re intending to be RESTful, because the organisation will come quite intuitively. I’m assuming a directory structure like this, where apples, oranges and pears are our resources:

├── index.js
├── routes.js
├── routes
│   ├── apples.js
│   ├── oranges.js
│   └── pears.js

In index.js, all we have to do is bootstrap the routes:

require("./routes")(app);

In routes.js, we require each route file iteratively. Take note that usual routing precedence rules apply - both the file order as well as the route order inside a file matters.

import express from "express";
const routes = [
  { mountPoint: "/apples", filePath: "./routes/apples" },
  { mountPoint: "/oranges", filePath: "./routes/oranges" },
  { mountPoint: "/pears", filePath: "./routes/pears" },
];
 
module.exports = app => {
  for (let route of routes) {
    let router = express.Router();
    require(route.filePath)(router);
    app.use(route.mountPoint, router);
  }
};

We can DRY it up even more by getting the file path from the mount point (or the other way around):

import express from "express";
const routesDirectory = "./routes";
const routes = ["apples", "oranges", "pears"];
 
module.exports = app => {
  for (let route of routes) {
    let router = express.Router();
    require(`${routesDirectory}/${route}`)(router);
    app.use(`/${route}`, router);
  }
};

There are many different ways to specify middleware for one or many route files. Here are some ideas:

import express from "express";
const routes = [
  { mountPoint: "/apples", filePath: "./routes/apples", auth: true },
  { mountPoint: "/oranges", filePath: "./routes/oranges" },
  { mountPoint: "/pears", filePath: "./routes/pears" },
];
 
module.exports = app => {
  for (let route of routes) {
    let router = express.Router();
 
    // this middleware is applied to all routes
    router.use(defaultMiddleware());
 
    // only routes with auth: true will have this middleware
    if (route.auth) {
      router.use(authMiddleware());
    }
 
    require(route.filePath)(router);
    app.use(route.mountPoint, router);
  }
};

We can run multiple separate loops:

import express from "express";
const routesDirectory = "./routes";
const routes = ["oranges", "pears"];
 
const authRoutes = ["apples"];
 
module.exports = app => {
  for (let route of routes) {
    let router = express.Router();
    require(`${routesDirectory}/${route}`)(router);
    app.use(`/${route}`, router);
  }
  for (let route of authRoutes) {
    let router = express.Router();
    router.use(authMiddleware());
    require(`${routesDirectory}/${route}`)(router);
    app.use(`/${route}`, router);
  }
};

We can even use regex (regex for flex!):

import express from "express";
const routesDirectory = "./routes";
const routes = ["apples", "oranges", "pears"];
const someRegex = /something/;
 
module.exports = app => {
  for (let route of routes) {
    let router = express.Router();
 
    if (someRegex.test(route)) {
      router.use(authMiddleware());
    }
 
    require(`${routesDirectory}/${route}`)(router);
    app.use(`/${route}`, router);
  }
};

Within a route file like apples.js, we can specify a middleware specific to the routes inside the file:

module.exports = router => {
  // only routes in apples.js will have this middleware
  router.use(specificMiddleware());
 
  router.get("/", (req, res) => {
    //...
  });
  router.post("/", (req, res) => {
    //...
  });
  //... other routes
};

And last but not least, if you only want to apply middleware to a single endpoint, you can do that too:

module.exports = router => {
  router.use(specificMiddleware());
 
  router.get("/", superSpecificMiddleware(), (req, res) => {
    //...
  });
  router.post("/", (req, res) => {
    //...
  });
  //... other routes
};

Well, I did mention flexibility, didn’t I?

Nested Routes

Since we’re on the topic of RESTful, how can we do nested routes?

Let’s say pineapples is a resource nested within apples, so we’ll want to have routes that look like:

GET /apples/:appleId/pineapples
POST /apples/:appleId/pineapples
GET /apples/:appleId/pineapples/:pineappleId
...

and so on.

You can add nested routers as middleware.

The key is to add mergeParams: true to the Router factory so that the nested router can access the params from its parent router, which in this case is appleId.

In apples.js, it’ll look like this:

const nestedRoutes = ["pineapples"];
 
module.exports = router => {
  for (let route of nestedRoutes) {
    let nestedRouter = express.Router({ mergeParams: true });
    require(`./${route}`)(nestedRouter);
    router.use(`/:appleId/${route}`, nestedRouter);
  }
 
  router.get("/", (req, res) => {
    //...do something
  });
  //... other apple routes
};

Of course, if your app is really complex, you can put nested route files inside folders:

├── index.js
├── routes.js
├── routes
│   ├── apples.js
│   ├── oranges.js
│   ├── pears.js
│   ├── apples
│   │   ├── pineapples.js
│   │   └── mangoes.js
│   └── oranges
│       └── starfruits.js

and change the code above to reflect this file structure.