Dynamic Route Registration in Vue.js Using Backend Permission Data
Role-based access control often requires generating navigation routes dynamically rather than hardcoding them in the frontend. By delegating route definitions to the back end, applications can enforce granular permissions without redeploying the client. The implementation revolves around intercepting navigation, fetching permission data, transforming it into valid route records, and registering them at runtime.
Dynamic Component Resolution
Backend systems cannot transmit Vue component instances. Instead, they provide string identifiers representing file paths. The frontend must map these strings to actual components using dynamic imports. A resolver function handles this conversion:
const resolveModule = (modulePath) => {
return () => import(`@/pages/${modulePath}.vue`);
};
Base Router Configuration
Initialize the router with static routes that require no authentication, such as login, public pages, and a fallback layout. These routes remain constant regardless of user permissions.
import { createRouter, createWebHistory } from 'vue-router';
import BaseLayout from '@/layouts/BaseLayout.vue';
export const staticRoutes = [
{
path: '/auth/login',
component: () => import('@/pages/auth/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: BaseLayout,
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/pages/Home.vue'),
meta: { title: 'Dashboard' }
}
]
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages/errors/NotFound.vue'),
meta: { hidden: true }
}
];
const router = createRouter({
history: createWebHistory(),
routes: staticRoutes,
scrollBehavior: () => ({ top: 0 })
});
export default router;
Navigation Guard Interception
Route generation should occur before the user accesses protected views. A global beforeEach guard checks authentication status and determines whether dynamic routes have already been registered. If not, it triggers the fetch and registration process.
import router from '@/router';
import { useAuthStore } from '@/stores/auth';
import { usePermissionStore } from '@/stores/permission';
import { getToken } from '@/utils/auth';
router.beforeEach(async (to, from, next) => {
const token = getToken();
const authStore = useAuthStore();
const permissionStore = usePermissionStore();
if (!token) {
return to.path === '/auth/login' ? next() : next('/auth/login');
}
if (to.path === '/auth/login') {
return next('/');
}
if (permissionStore.isRoutesLoaded) {
return next();
}
try {
const accessList = await authStore.fetchUserPermissions();
const dynamicRoutes = permissionStore.buildRouteTree(accessList);
dynamicRoutes.forEach(route => router.addRoute(route));
permissionStore.markRoutesLoaded();
next({ ...to, replace: true });
} catch (error) {
await authStore.logout();
next('/auth/login');
}
});
State Management and Route Transformation
The raw data from the API requires recursive processing to match Vue Router's configuration schema. A dedicated store module handles the transformation, assigns resolved components, and merges the dynamic routes with the static configuration.
import { defineStore } from 'pinia';
import { staticRoutes } from '@/router';
import BaseLayout from '@/layouts/BaseLayout.vue';
export const usePermissionStore = defineStore('permission', {
state: () => ({
dynamicRoutes: [],
isRoutesLoaded: false
}),
actions: {
buildRouteTree(apiData) {
const transformed = this.transformNodes(apiData);
this.dynamicRoutes = transformed;
return transformed;
},
transformNodes(nodes) {
return nodes.map(node => {
const routeConfig = {
path: node.path,
name: node.routeName,
meta: {
title: node.label,
icon: node.icon || 'default-icon',
requiresAuth: true
}
};
if (node.viewKey === 'BaseLayout') {
routeConfig.component = BaseLayout;
} else if (node.viewKey) {
routeConfig.component = () => import(`@/pages/${node.viewKey}.vue`);
}
if (Array.isArray(node.subRoutes) && node.subRoutes.length > 0) {
routeConfig.children = this.transformNodes(node.subRoutes);
}
return routeConfig;
});
},
markRoutesLoaded() {
this.isRoutesLoaded = true;
}
}
});
The recursive transformer iterates through the backend payload, maps string identifiers to lazy-loaded components, attaches metadata, and constructs nested child routes. Once processed, the guard registers each route individually and redirects the user to the originally requested path, ensuring the navigation pipeline recognizes the newly added definitions.