Dynamically Generating Vue Router Routes From Directory Structure
Tags: Vue, javascript
Published: 2020-05-03 18:00:29

I'm working on a Vue starter kit for a Javascript platform that I've used for a few years, and I have the need - well, the desire to make it as easy for the developer as possible - to automatically import route definitions from multiple locations and add them to the Router() definition, instead of loading them all from the router index file.

In applications such as Vue apps that have multiple types of files that are used together to create the app, there are primarily two ways to organize the files. For instance, in Vue, let's say that we have multiple resources, and each resource has three components:

1. The component (.vue file)
2. The router definition file
3. The Vuex module file

You could choose to organize your files in two different ways:

- By feature - each directory has a .vue file, a router file, and a Vuex file
- By functionality - one directory each for components, one for router files, and one for Vuex files.

In my case, I'm going to group by feature, which I will refer to as a resource (to match the platform naming convention). Using the example resources, of user, event, and job, this will be my directory structure:
```
/src
  |
  ---/resources
         |
         ---/user
              |
               ---User.vue
               ---routes.js
               ---store.js
            /event
              |
               ---Event.vue
               ---routes.js
               ---store.js
            /job
              |
               ---Job.vue
               ---routes.js
               ---store.js
```
The contents of a `routes.js` file will look like this:
```
import Event from '@/resources/event/Event'

export default [
  {
    path: '/events',
    name: 'event',
    component: Event
  },
];
```
Including an additional `Auth.vue` component (which is outside the `/resources` directory), our base router index file will look like this:
```
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Auth from '@/components/Auth';

Vue.use(Router);

let routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/login',
    name: 'auth',
   component: Auth
  },
];

export default new Router({
  mode: 'history',
  routes,
})
```
If I were to manually add the router objects for my three resources, I could do it like this:
```
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Auth from '@/components/Auth';
import Event from '@/resources/event/Event';
import Job from '@/resources/job/Job';
import User from '@/resources/user/User'; 

Vue.use(Router);

let routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/login',
    name: 'auth',
    component: Auth
  },
  {
    path: '/events,
    name: 'event',
    component: Event
  },
  {
    path: '/Job',
    name: 'job',
    component: Job
  },
  {
    path: '/user',
    name: 'user',
    component: User
  },
];

export default new Router({
  mode: 'history',
  routes,
})
```
However, I want to avoid having to manually update this file every time I add a resource, so instead I do it dynamically:
```
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Auth from '@/components/Auth';

Vue.use(Router);

let baseRoutes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/login',
    name: 'auth',
    component: Auth
  },
];

// Import all of the resource routes files.
function loadRoutes() {
  const context = require.context('@/resources', true, /routes.js$/i)
  return context.keys()
    .map(context) // import module
    .map(m => m.default) // get `default` export from each resolved module
}

const resourceRoutes = loadRoutes();
resourceRoutes.forEach((route) => {
  routes.push(route[0]);
});

export default new Router({
  mode: 'history',
  routes,
})
```
The magic happens in the `loadRoutes()` function. As described to me in the [answer to my Stackoverflow post](https://stackoverflow.com/a/61262665/391054), `require.context()` returns a context module, which exports a `keys` function that returns an array of all possible requests (i.e., the matching paths), each of which could be passed to the context itself (which invokes its resolve function) to import the corresponding module. The returned array from `loadRoutes()` function looks like this:
```
resourceRoutes = [
  {
    0: [
          {
            path: "/event",
            name: "event",
           component: {
             name: "Event",
             ...
           }
        ]
  },
  {
    1: [
          {
            path: "/jobs",
            name: "job",
           component: {
             name: "Job",
             ...
           }
        ]
  },
...
]
```
so I just loop through each returned item, get the first array item, and add it to the routes array. And voila!, it's done.

There are a couple benefits to this approach. First, it keeps my code modularized and keeps my main router file from getting more and more cluttered. Second, this looks toward the future; down the road, I will be creating a Vue CLI plugin that will allow a user to add all of the necessary files for a new resource that has been added to the platform, and it is easier to create a new file with a pre-determined content than it is to modify the contents of an existing file.