← Back to Posts

Nested routes in Express v5

October 18, 2024

Express v5 was released recently (everyone cheer!)

And as I'm using Express as the backend to my Visual page builder (Juno), I decided to look for a solution to nested routing which many other frameworks have.

What is nested routing?

Nested routing is when a set of routes share the same base path, or heirarchy,

For example, /admin/posts and /admin/users share the /admin base path, so they can be nested under /admin in the routing configuration.

In frameworks such as Laravel, this can typically be done using route->group(), whilst in Folder-based routers (such as Nuxt), you'd create folders to store each endpoint function in.

However, Express hasn't got the same capability just yet so here's how I implemented the same functionality in v5.

Extending the Express Router

The magic behind nested routes is to extend the Express' Router to add a "group()" method to it. Within this method, a new child Router is created and then applying the nested prefix to it. Below is an example of this code:

CustomRouter.ts

function extendRouter() {
  const router = Router();

  router.group = function (prefix: string, callback: (router: Router) => void) {
    const childRouter = Router();

    callback(childRouter);
    this.use(prefix, childRouter);
  };

  return router;
}

export const AppRouter = extendRouter();

If you're using Typescript, you'll likely need to add the group method to Express' types;

CustomRouter.ts

declare module "express-serve-static-core" {
  interface Router {
    group(prefix: string, callback: (router: Router) => void): void;
  }
}

Using the group() function

Once you've extended the Router function, you're set to use it anywhere in your Express app to organise routes.

routes.ts

import { AppRouter } from "./router";
import { Request, Response } from "express";

AppRouter.group("/custom", router => {
  router.use((req: Request, res: Response, next: Function) => {
    console.log("Custom middleware");
    next();
  });

  router.get("/test", (req: Request, res: Response) => {
    res.send("Hello from custom route!");
  });

  router.post("/post", (req: Request, res: Response) => {
    res.send("Hello from custom route!");
  });
});

export default AppRouter;

index.ts

app.use(customRouter);

The cool part of this is that the child router is just another Express router, so you can apply middleware in the same way you'd apply it normally with router.use()

After thoughts

This is a neat feature that helps to clean up and manage routes. However, routes can also be grouped into separate files with router.use() without any custom code, so it's really a personal preference whether someone uses .use() or implements .group(), the beauty of Express is that the choice is yours.

Happy coding, Wizards! 🧙‍♂️