Building a nested routes function - a fun exercise

I was reading the Remix documentation and saw a function that they use to programatically define routes. It's pretty cool in that you are given a single function to build your routes, but you can nest these function calls to build and group relative paths together. It got me thinking about how I might implement something like that, so I gave it a shot.

For context, here's what an example of calling this function might look like:

const routes = defineRoutes((route) => {
  route("users", "user.js");
  route("users/:id", "userShow.js");

  route("companies/:id", "companies.js", () => {
    route("users", "companyUsers.js");
    route("stores", "companyStores.js");
    route("stores/:id", "companyStore.js", () => {
      route("products", "companyStoreProduct.js");
    });
  });

  route("roles/:id", "roles.js", () => {
    route("users", "usersForRole.js");
  });
});

console.log(routes);
// output
[
  { path: "users", file: "user.js" },
  { path: "users/:id", file: "userShow.js" },
  { path: "companies/:id", file: "companies.js" },
  { path: "companies/:id/users", file: "companyUsers.js" },
  { path: "companies/:id/stores", file: "companyStores.js" },
  { path: "companies/:id/stores/:id", file: "companyStore.js" },
  { path: "companies/:id/stores/:id/products", file: "companyStoreProduct.js" },
  { path: "roles/:id", file: "roles.js" },
  { path: "roles/:id/users", file: "usersForRole.js" },
];

As you can see, top level routes are stored as-is but as you nest routes deeper and deeper within the route callback function you can build off of the parent path without having to type it every time.

There's a few different concepts you have to deal with while implementing this which makes me think it would make a great interview question to pair on with a candidate.

I'll post my solution below here, but try it out yourself before looking at mine and let me know how it went! Make sure to test it against multiple side-by-side nestings like I did above to make sure your paths are building properly.

function defineRoutes(cb) {
  const routes = [];
  let parent = "";

  function pathWithParent(path) {
    if (parent) {
      return `${parent}/${path}`;
    }
    return path;
  }

  function route(path, file, nestedCb) {
    routes.push({ path: pathWithParent(path), file });
    if (nestedCb) {
      parent = pathWithParent(path);
      nestedCb();
      parent = parent.slice(0, -(path.length + 1));
    }
  }

  cb(route);

  return routes;
}