Appearance
Routing
Vue Router is the core navigation system in a Vue 3 application. The following guidelines reflect how we set up and use routes across this repo.
Router Configuration
- Keep the router instance in
src/router/. - Export it as the default export so that
main.tscan import it directly. - Use history mode (
createWebHistory) for clean URLs (no hash). - Register global navigation guards in this file; keep guard logic in separate modules if they grow large.
- Mature-hybrid exception: A legacy or server-rendered application may import feature route modules directly from
src/router/index.tsinstead of merging them throughsrc/router/routes.ts. It may also keep eager bootstrap, shell, layout, and bridge components in the router entry point, and keep named guards in oneguards.tsmodule. Use this topology only when the application must bridge Vue routes with server-owned routes. Keep the greenfield topology above as the default for new applications. - Treat the router default export as a scoped exception to the named-export rule in
docs/conventions/01.Naming.md. The default export lets the application entry point import the configured router directly. Keep other router utilities and route collections named unless this guide says otherwise.
ts
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes,
})
export default routerDefining Routes
Store route definitions in
src/router/routes.tsas an array ofRouteRecordRaw.Give each module its own
src/router/routes/[module].tsfile (camelCase name).- Export the module routes as the default export, then import them in
routes.tsand spread them into the main routes array.
ts// src/router/routes/auth.ts import type { RouteRecordRaw } from 'vue-router' import { ROUTE_NAMES } from '@/config/routes/names' const authRoutes: RouteRecordRaw[] = [ { path: '/login', name: ROUTE_NAMES.AUTH_LOGIN, component: () => import('@/pages/auth/LoginPage.vue'), }, ] export default authRoutes- Export the module routes as the default export, then import them in
Use component lazy-loading (dynamic
import()) for every ordinary page boundary to keep the initial bundle small. Eagerly import a shell, bootstrap component, authentication callback, layout, or Vue/server bridge when it must be available during application startup or a cross-application handoff. Do not make every component lazy when the boundary itself must load before navigation can proceed.Keep path names kebab-case and nested routes inside a parent if they belong to the same feature folder.
Declare meta properties (
requiresAuth,title, etc.) on route objects; this keeps guard logic in one place.Extend Vue Router's
RouteMetatype to document approved meta flags and catch typos at compile time.Prefer
redirect: { name: ROUTE_NAMES.SOME_ROUTE }instead of hard-coded paths so route URLs can change without breaking navigation. A named redirect is a preference, not a requirement: retain a path redirect when it targets a server-owned route, a legacy URL, or a deliberate external handoff.
Route Page Components
- Mirror the naming rules from the naming convention guide (
docs/conventions/01.Naming.md).- Page files live in
src/pages/<Feature>/<Feature>.vue(PascalCase) and must communicate the real feature/module. - Avoid placeholders such as
Index.vueor suffixes likePage.vue; the filename should match the component imported by the route.
- Page files live in
- Ensure the route component imports use the matching PascalCase component filenames.
✅ Good
text
src/pages/Checkout/Checkout.vue
src/pages/UserProfile/UserProfile.vue❌ Bad
text
src/pages/Checkout/Index.vue
src/pages/user-profile/UserProfilePage.vueRoutes configuration
ts
// src/router/routes.ts
import type { RouteRecordRaw } from 'vue-router'
import authRoutes from './routes/auth'
import profileRoutes from './routes/profile'
import errorRoutes from './routes/errors'
import { ROUTE_NAMES } from '@/config/routes/names'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: ROUTE_NAMES.ROOT_HOME,
component: () => import('@/pages/Home.vue'),
meta: { title: 'Home' },
},
{
path: '/about',
name: ROUTE_NAMES.ROOT_ABOUT,
component: () => import('@/pages/About.vue'),
meta: { title: 'About' },
},
...authRoutes,
...profileRoutes,
...errorRoutes,
{
path: '/:pathMatch(.*)*',
redirect: { name: ROUTE_NAMES.ERROR_NOT_FOUND },
},
]
export default routesNested Routes with Shared Guards
- A parent route can carry guards and
metathat every child route inherits. - Put shared access-control and common data-fetch on the parent. Keep child routes focused on their own page.
- The parent's
beforeEnterruns when navigation enters the parent route record, before the child route resolves. - It does not rerun for child-to-child navigation or parameter, query, or hash updates while the parent remains active, unless the navigation enters the parent route record again. Use a global guard, a child guard, or an in-component update guard when that transition needs a check.
For company-scoped or similarly nested features, keep the scope in the parent path and spread feature routes into its children array:
ts
const routes: RouteRecordRaw[] = [
{
path: '/c/:company',
component: () => import('@/pages/Company/Company.vue'),
children: [...settingsRoutes, ...ordersRoutes],
},
]ts
// src/router/routes.ts
import { authGuard, fetchCommonData } from '@/router/guards'
const routes: RouteRecordRaw[] = [
{
path: '/c/:company',
component: () => import('@/pages/Company/Company.vue'),
beforeEnter: [authGuard, fetchCommonData],
children: [
...companyRoutes,
...settingsRoutes,
...orderRoutes,
],
},
]Routing Names
- Route names are defined as constants in the
src/config/routesfolder, grouped by module.- Example:
src/config/routes/auth.ts - For application level related routes, use
src/config/routes/root.ts
- Example:
- Each module file exports a screaming-case constant named
[MODULE]_ROUTE_NAMES. Useas constto ensure there will be no unwanted mutations on the constant. - The constant is an object where each key is the route name in screaming case and the value is the kebab-case route-name value used in Vue Router. The value is a name, not a URL path, even when both use kebab-case.
- The keys need not match the values; this allows semantic naming independent of URL shape. Keep URL paths in the route record's
pathfield. - Prefix each key with the module name to prevent collisions (
[MODULE]_[ROUTE NAME]), and mirror that prefix in the value in kebab-case, so final route names stay unique when merged.
ts
// src/config/routes/auth.ts
export const AUTH_ROUTE_NAMES = {
AUTH_LOGIN: 'auth-login',
AUTH_REGISTER: 'auth-register',
AUTH_FORGOT_PASSWORD: 'auth-forgot-password',
AUTH_RESET_PASSWORD: 'auth-reset-password',
} as constChild Modules
Keep the module constant flat—no nested objects. For submodules, extend the prefix ([MODULE]_[SUB MODULE]_[ROUTE NAME]) and reflect it in the value in kebab-case.
ts
// ✅ Good: flat keys and values include module/submodule prefixes
export const AUTH_ADMIN_ROUTE_NAMES = {
AUTH_ADMIN_DASHBOARD: 'auth-admin-dashboard',
AUTH_ADMIN_USERS_LIST: 'auth-admin-users-list',
} as const
// ❌ Bad: nesting even though keys/values follow the convention
// Reason it's bad: the constant must stay flat; nesting complicates imports/merges.
export const AUTH_ADMIN_ROUTE_NAMES = {
AUTH_ADMIN_DASHBOARD: 'auth-admin-dashboard',
AUTH_ADMIN_USERS: {
AUTH_ADMIN_USERS_LIST: 'auth-admin-users-list',
AUTH_ADMIN_USERS_DETAIL: 'auth-admin-users-detail',
},
} as constMerging Route Name Constants
- Each module exports its own
[MODULE]_ROUTE_NAMES. A barrel file spreads them into oneROUTE_NAMESobject that the rest of the app imports. - Spreading keeps the constant flat; module prefixes in keys guarantee no collisions.
ts
// src/config/routes/names.ts
import { AUTH_ROUTE_NAMES } from './auth'
import { ROOT_ROUTE_NAMES } from './root'
import { ERROR_ROUTE_NAMES } from './errors'
export const ROUTE_NAMES = {
...ROOT_ROUTE_NAMES,
...AUTH_ROUTE_NAMES,
...ERROR_ROUTE_NAMES,
} as constNavigation Guards
- Place every guard under
src/router/guards/<intent>.ts, one guard per file. - Name the file after the guard intent in camelCase without the
Guardsuffix (auth.ts,metaTitle.ts). Prefer single-word intents when possible and ensure the name communicates the guard's responsibility. - Default-export each guard; avoid named exports unless multiple exports are truly required.
- Name the guard function after the intent and suffix it with
Guard(e.g.,authGuard,metaTitleGuard). - Keep guards focused on allowing, blocking, or redirecting navigation; push data preloading or expensive async work to global resolve guards.
- Typical use cases:
- Authentication guard – run before each navigation to check
requiresAuthmeta and redirect to/loginif not authenticated. - Meta-title guard – set
document.titleafter every successful route change.
- Authentication guard – run before each navigation to check
- Keep the lifecycle distinction clear: use
beforeEachfor application-wide blocking or redirecting checks,beforeEnterfor route-record checks,beforeResolvefor checks or preparation that must run after async route components and in-component guards, andafterEachfor confirmed-navigation side effects. - Do not use
afterEachto block navigation. Its work must tolerate a confirmed route and must not be required for the navigation to succeed. Handle errors from analytics, title updates, notifications, or similar side effects without turning them into navigation failures.
Example of a guard function
ts
// src/router/guards/auth.ts
import type { NavigationGuard } from 'vue-router'
import { useAuthStore } from '@/stores/auth.store'
const authGuard: NavigationGuard = (to, _from, next) => {
const store = useAuthStore()
if (to.meta.requiresAuth && !store.profile) {
return next('/login')
}
next()
}
export default authGuardAdding guard to the router instance.
Attach guards with the lifecycle that matches their intent: beforeEach for blocking/redirecting logic (most of our guards), and afterEach for side effects that need the confirmed navigation. In practice, the majority of guards should be registered via beforeEach.
ts
// src/router/index.ts
import authGuard from './guards/auth'
// ...
router.beforeEach(authGuard)
// ...
export default routerPermissions
- Declare permissions on
route.meta.permissionsas an array. A single global guard evaluates them. Route authors declare permissions; they do not write guard logic. - Register the permissions guard with
beforeResolve, notbeforeEach, when its evaluator depends on data loaded by earlier guards or route resolution.beforeResolveruns later in the lifecycle, but it does not guarantee that an application has loaded every required store. The earlier guard or resolver must explicitly load that data. Keep access-control that needs no async data inbeforeEach. - The guard flattens permissions from
to.matched—the array of matched route records from parent to child. A permission on a parent route applies to every child route. This is what makes the declarative approach work for nested routes.
Define the rule and permission types once, alongside the router type augmentation:
ts
// src/types/router.d.ts
import type { RouteRecordRedirectOption } from 'vue-router'
export interface PermissionContext {
user: { permissions: Record<string, boolean>; featureFlags: Record<string, boolean> }
userActiveCompany: { staffIsAdmin: boolean; staffId?: string } | undefined
activeCompany: { buyer?: boolean; seller?: boolean } | undefined
}
export type PermissionRule = string | ((ctx: PermissionContext) => boolean)
export interface RoutePermission {
rules: PermissionRule[]
redirect?: RouteRecordRedirectOption
}
// Adapt the generic route contract to a project-specific evaluator.
export type PermissionEvaluator = (rule: PermissionRule, ctx: PermissionContext) => booleanPut the runtime adapter in a .ts module, not in the declaration file:
ts
// src/router/permissions.ts
import type { PermissionEvaluator } from '@/types/router'
export const evaluatePermission: PermissionEvaluator = (rule, ctx) => {
if (typeof rule === 'function') return rule(ctx)
return Boolean(ctx.user.permissions[rule] ?? ctx.user.featureFlags[rule])
}The adapter keeps route metadata generic while allowing a project evaluator to combine its own permission stores, feature flags, or account state. Keep company, staff, buyer, and seller fields only when the application needs them. Do not claim that beforeResolve loaded the context unless an earlier guard or resolver did so.
- String rules match a permission key or feature flag. Function rules run a custom predicate with the typed context. Use a string when a key check is enough; use a function for compound conditions.
- The
redirectfield accepts a route-location object or a function(to) => routeLocationfor dynamic redirects:
ts
redirect: (to) => ({ name: ROUTE_NAMES.ERROR_FORBIDDEN, query: { from: to.fullPath } })A single global guard evaluates every permission on the matched routes:
ts
// src/router/guards/permissions.ts
import type { NavigationGuard } from 'vue-router'
import { storeToRefs } from 'pinia'
import { ROUTE_NAMES } from '@/config/routes/names'
import { useUserStore } from '@/stores/user.store'
import { useCompanyStore } from '@/stores/company.store'
import { evaluatePermission } from '@/router/permissions'
import type { PermissionContext, RoutePermission } from '@/types/router'
const permissionsGuard: NavigationGuard = (to) => {
const routePermissions = to.matched
.filter((record) => record.meta.permissions)
.flatMap((record) => record.meta.permissions as RoutePermission[])
if (!routePermissions.length) return
const userStore = useUserStore()
const companyStore = useCompanyStore()
const { user, userActiveCompany } = storeToRefs(userStore)
const { activeCompany } = storeToRefs(companyStore)
const ctx: PermissionContext = {
user: user.value,
userActiveCompany: userActiveCompany.value,
activeCompany: activeCompany.value,
}
for (const permission of routePermissions) {
const passed = permission.rules.every((rule) => evaluatePermission(rule, ctx))
if (!passed) {
if (permission.redirect) {
return typeof permission.redirect === 'function'
? permission.redirect(to)
: permission.redirect
}
return { name: ROUTE_NAMES.ERROR_FORBIDDEN }
}
}
}
export default permissionsGuardRegister the guard on beforeResolve:
ts
// src/router/index.ts
import permissionsGuard from './guards/permissions'
// ...
router.beforeResolve(permissionsGuard)
// ...
export default routerDeclare permissions on the route. The guard does the rest:
ts
{
path: '/settings/billing',
name: ROUTE_NAMES.SETTINGS_BILLING,
component: () => import('@/pages/Settings/Billing/Billing.vue'),
meta: {
permissions: [
{ rules: ['canManageBilling'] },
{
rules: [({ userActiveCompany }) => Boolean(userActiveCompany?.staffIsAdmin)],
redirect: { name: ROUTE_NAMES.ERROR_FORBIDDEN },
},
],
},
}Global Resolve Guards
- Use resolve guards for async data preparation that must finish after navigation guards succeed but before the component renders; keep access-control logic in navigation guards.
- Store resolve guards under
src/router/resolvers/<intent>.ts, one resolver per file. - Name the file in camelCase without the
Resolversuffix (product.ts,dataPrefetch.ts). - Default-export each resolver function.
- Suffix the resolver function name with
Resolverto reflect its purpose (e.g.,productResolver). - Register resolve guards with
router.beforeResolvewhen you need to wait for async operations after in-component guards but before navigation finalizes.
Example of a resolve guard
ts
// src/router/resolvers/product.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useProductStore } from '@/stores/product.store'
const productResolver = async (
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
const store = useProductStore()
const productId = String(to.params.id)
await store.fetchProduct(productId)
next()
}
export default productResolverAdding resolver to the router instance.
ts
// src/router/index.ts
import productResolver from './resolvers/product'
// ...
router.beforeResolve(productResolver)
// ...
export default routerFor a route-specific permission resolver, restore the requiredPermission contract when a single permission key is enough. The resolver must load the permission store before it checks access:
ts
// src/router/resolvers/permissions.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { usePermissionsStore } from '@/stores/permissions.store'
import { ROUTE_NAMES } from '@/config/routes/names'
const permissionsResolver = async (
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
const store = usePermissionsStore()
if (to.meta.requiredPermission) {
await store.ensureLoaded()
if (!store.hasPermission(to.meta.requiredPermission)) {
return next({ name: ROUTE_NAMES.ERROR_FORBIDDEN })
}
}
next()
}
export default permissionsResolverRegister it with beforeResolve:
ts
// src/router/index.ts
import permissionsResolver from './resolvers/permissions'
// ...
router.beforeResolve(permissionsResolver)
// ...
export default routerPer-Route Guards
- Use inline
beforeEnterguards inside the route definition to handle simple, route-specific checks (e.g., permission gating for a single page). - Inline guards should remain concise; keep the logic focused on reading meta/config and deciding whether to allow navigation.
- When the logic grows beyond a handful of lines or is reused across routes, extract it to
src/router/guards/<intent>.tsand follow the navigation guard conventions above.
Example of an inline per-route guard
ts
// src/router/routes/profile.ts
import type { RouteRecordRaw } from 'vue-router'
import { usePermissionsStore } from '@/stores/permissions.store'
import { ROUTE_NAMES } from '@/config/routes/names'
const profileRoutes: RouteRecordRaw[] = [
{
path: '/profile',
name: ROUTE_NAMES.PROFILE_ROOT,
component: () => import('@/pages/Profile/Profile.vue'),
beforeEnter: async (to, _from, next) => {
const store = usePermissionsStore()
await store.ensureLoaded()
if (!store.hasPermission('profile:read')) {
return next({ name: ROUTE_NAMES.ERROR_FORBIDDEN })
}
next()
},
},
]
export default profileRoutesGuard Arrays (Route Middleware)
beforeEnteraccepts an array of guard functions that run in order. This is the route-middleware pattern.- Reusable guards exported from
src/router/guards/compose as arrays on routes, chaining auth, data-fetch, and permission checks without inline logic. If a guard in the chain cancels navigation, the remaining guards do not run. - Use arrays when two or more guards apply to a route. Use a single inline function only for one-off logic that no other route needs.
ts
// src/router/routes/profile.ts
import { authGuard, fetchProfileData } from '@/router/guards'
import type { RouteRecordRaw } from 'vue-router'
import { ROUTE_NAMES } from '@/config/routes/names'
const profileRoutes: RouteRecordRaw[] = [
{
path: '/profile',
name: ROUTE_NAMES.PROFILE_ROOT,
component: () => import('@/pages/Profile/Profile.vue'),
beforeEnter: [authGuard, fetchProfileData],
},
]In-Component Guards
- Avoid using
beforeRouteEnter,beforeRouteUpdate, orbeforeRouteLeaveinside components unless the logic depends on component instance state and cannot live in a route, module, or global guard. - If a component-level guard is justified, document the reason with a code comment immediately above the guard so the intent stays clear.
- Prefer delegating to composables or shared guards rather than embedding complex navigation logic directly in components.
Example of a justified component guard
ts
// src/pages/Wizard/Wizard.vue
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const hasUnsavedWork = ref(false)
// Guard is local because it needs direct access to reactive form state
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedWork.value && !window.confirm('You have unsaved changes. Leave anyway?')) {
return next(false)
}
next()
})
</script>Accessing Router Inside Components
- Always use the Composition API helpers to interact with Vue Router inside components.
const router = useRouter();gives access to navigation helpers likerouter.pushwhile preserving the correct instance.const route = useRoute();exposes reactive information about the active route (params,query,meta, etc.).
- Navigations (
router.push,<router-link>, redirects) should reference route-name constants exported fromsrc/config/routes/*to avoid drift when URLs change.
vue
<router-link :to="{ name: ROUTE_NAMES.CHECKOUT_ROOT }">Checkout</router-link>ts
router.push({ name: ROUTE_NAMES.ERROR_NOT_FOUND })- Example usage:
ts
// src/pages/Checkout/Checkout.vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const selectedTab = computed(() => route.query.tab ?? 'summary')
function goToConfirmation() {
router.push({ name: ROUTE_NAMES.CHECKOUT_CONFIRMATION, params: { orderId: route.params.orderId } })
}
</script>Typing params and queries
- Narrow route params and query objects to avoid typos and missing keys.
- Reuse Vue Router types like
RouteLocationNormalizedLoaded['params']to infer shapes from route records. - When parameters are optional, coerce them with explicit fallbacks or zod-style validators.
ts
import type { RouteLocationNormalizedLoaded } from 'vue-router'
type CheckoutParams = RouteLocationNormalizedLoaded['params'] & { orderId: string }
const route = useRoute()
const params = route.params as CheckoutParams
router.push({
name: ROUTE_NAMES.CHECKOUT_CONFIRMATION,
params: { orderId: params.orderId },
})Route params and query values remain untyped at runtime (string | string[]). Treat the intersection above as a compile-time contract, not runtime validation. Use String(...) or explicit array handling to coerce values into the shape the consumer needs.
ts
const route = useRoute()
const orderId = String(route.params.orderId)
router.push({
name: ROUTE_NAMES.CHECKOUT_CONFIRMATION,
params: { orderId },
})- Vue Router offers typed routes via
createRoutergenerics, but runtime coercion is the pragmatic default for most projects.
Error & Fallback Routes
- Keep dedicated error and fallback routes in
src/router/routes/errors.tsso every module can reference them consistently. - Provide named routes for common scenarios such as forbidden access, not-found pages, and generic failures.
- Use a catch-all path (
/:pathMatch(.*)*) to handle unknown URLs and render a not-found experience.
ts
// src/router/routes/errors.ts
import type { RouteRecordRaw } from 'vue-router'
import { ROUTE_NAMES } from '@/config/routes/names'
const errorRoutes: RouteRecordRaw[] = [
{
path: '/forbidden',
name: ROUTE_NAMES.ERROR_FORBIDDEN,
component: () => import('@/pages/Errors/Forbidden.vue'),
meta: { title: 'Forbidden' },
},
{
path: '/error',
name: ROUTE_NAMES.ERROR_GENERIC,
component: () => import('@/pages/Errors/Error.vue'),
meta: { title: 'Something went wrong' },
},
{
path: '/not-found',
name: ROUTE_NAMES.ERROR_NOT_FOUND,
component: () => import('@/pages/Errors/NotFound.vue'),
meta: { title: 'Page not found' },
},
]
export default errorRoutesLazy Loading and Prefetching
- Use
route.meta.prefetchflag to trigger pre-fetch of data for a route as soon as the user hovers over a link (using a custom directive such asv-prefetch). - In guard or component, check this flag and call the corresponding composable/service.
ts
// src/directives/prefetch.ts
import type { Directive } from 'vue'
import router from '@/router'
type PrefetchElement = HTMLElement & { __prefetchHandler__?: () => void }
export const prefetch: Directive<PrefetchElement, string> = {
beforeMount(el, binding) {
const handler = () => {
const routeName = binding.value
const targetRoute = router.resolve({ name: routeName })
if (targetRoute.meta.prefetch) {
// Call domain-specific prefetch logic here
}
}
el.__prefetchHandler__ = handler
el.addEventListener('mouseenter', handler)
},
unmounted(el) {
if (el.__prefetchHandler__) {
el.removeEventListener('mouseenter', el.__prefetchHandler__)
delete el.__prefetchHandler__
}
},
}ts
// src/main.ts
import { prefetch } from '@/directives/prefetch'
app.directive('prefetch', prefetch)Typing Route Meta
Declare every custom meta property in the Vue Router module augmentation so editors surface the approved keys.
ts
// src/types/route-meta.d.ts
import 'vue-router'
import type { RoutePermission } from './router'
declare module 'vue-router' {
interface RouteMeta {
title?: string
requiresAuth?: boolean
requiredPermission?: string
permissions?: RoutePermission[]
prefetch?: boolean | string
layout?: 'default' | 'full'
analyticsName?: string
/** Marketplace-compatible server-rendered boundary marker. */
hasServerRenderedContent?: boolean | (() => boolean)
/** Project-neutral alternative when the reference router marker is not used. */
serverRendered?: boolean
}
}Use one complete meta contract when a route needs several cross-cutting concerns. Keep the keys project-approved and type them in the module augmentation:
ts
{
path: '/settings/billing',
name: ROUTE_NAMES.SETTINGS_BILLING,
component: () => import('@/pages/Settings/Billing/Billing.vue'),
meta: {
layout: 'default',
title: 'Billing',
requiresAuth: true,
permissions: [{ rules: ['canManageBilling'] }],
analyticsName: 'billing',
// Reference-router name and type. The adapter may also provide a function.
hasServerRenderedContent: false,
},
}Full Navigation Resolution Flow
- Navigation triggered.
- Call
beforeRouteLeaveguards in deactivated components. - Call global
beforeEachguards. - Call
beforeRouteUpdateguards in reused components. - Call
beforeEnterguards declared on route records. - Resolve async route components.
- Call
beforeRouteEnterguards in activated components. - Call global
beforeResolveguards. - Navigation is confirmed.
- Call global
afterEachhooks. - DOM updates trigger.
- Call callbacks passed to
nextinbeforeRouteEnterguards with instantiated component instances.
Hybrid Routing and Server-Rendered Boundaries
- Keep the greenfield route topology as the recommended target. A mature hybrid application may import feature modules directly into
src/router/index.tswhen that is the established ownership boundary. Do not copy this exception into a new application without a server or legacy integration need. - Use an eager component for the application shell, bootstrap path, authentication callback, layout, or a
RouterViewbridge when the component connects Vue to an existing host. Lazy-load ordinary Vue page boundaries. - Mark routes that cross a server-rendered boundary with a typed meta flag. In the reference router, use
hasServerRenderedContent, which accepts a boolean or a zero-argument function. A project may use the neutral nameserverRenderedinstead, but its adapter must map that name to the reference router'shasServerRenderedContentcheck. Do not treat either flag as a substitute for the navigation guard that enforces the handoff. - A bridge route may render a
RouterViewfor Vue-owned children while handing other routes to Django or another server owner. Keep the bridge component and ownership rule close to the router configuration. - When a navigation crosses from a Vue page to a non-Vue page, or back, perform a full reload when the host must provide different HTML. A query-string or hash-only change on the same server-owned path may not require a reload when the host treats that change as in-page state.
- Keep server-rendered route behavior in a named guard or adapter. Do not make hybrid markers, eager bridge components, or full reloads mandatory for greenfield Vue-only applications.
Rationale
- Splitting routes by module keeps ownership clear, reduces merge conflicts, and makes it easy to extend the main route map with spreads.
- Aligning page component filenames with their features ensures imports are unambiguous and mirrors the routing configuration.
- Accessing the router through
useRouter()/useRoute()preserves the active instance, keeps components test-friendly, and avoids coupling to global singletons. - Co-locating one guard or resolver per intent file keeps navigation logic discoverable, encourages reuse, and simplifies testing and tree-shaking.
- Favoring concise per-route guards and avoiding in-component guards unless necessary keeps navigation concerns outside view logic and prevents duplicate permission checks.
- Declaring permissions on
route.metaand evaluating them with one global guard removes per-route permission logic from individual route definitions. - Composing reusable guards as
beforeEnterarrays lets shared access-control and data-fetch steps live in one place and apply uniformly across routes.