Frontend Engineeringintermediate
Vue Router 4: Navigation, Guards, Lazy Loading & Meta Fields
Build multi-page Vue 3 apps with Vue Router 4 — define routes, use dynamic segments, implement navigation guards for auth, lazy-load route components, use route meta fields, and handle nested layouts.
LearnixoApril 16, 20265 min read
Vue.jsVue RouterVue 3SPANavigationTypeScriptFrontend
Installation & Setup
Bash
npm install vue-router@4TYPESCRIPT
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition // browser back/forward
if (to.hash) return { el: to.hash, behavior: 'smooth' }
return { top: 0 }
},
})
export default routerTYPESCRIPT
// src/main.ts
import { createApp } from 'vue'
import router from './router'
app.use(router)Route Definitions
TYPESCRIPT
// src/router/routes.ts
import type { RouteRecordRaw } from 'vue-router'
export const routes: RouteRecordRaw[] = [
// Public routes
{
path: '/login',
name: 'Login',
component: () => import('@/pages/Login.vue'),
meta: { requiresAuth: false, layout: 'blank' },
},
// Protected routes — nested under AppLayout
{
path: '/',
component: () => import('@/layouts/AppLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/pages/Dashboard.vue'),
meta: { title: 'Dashboard', icon: 'LayoutDashboard' },
},
{
path: 'appointments',
name: 'Appointments',
component: () => import('@/pages/Appointments.vue'),
meta: { title: 'Appointments' },
},
{
path: 'appointments/:id',
name: 'AppointmentDetail',
component: () => import('@/pages/AppointmentDetail.vue'),
props: true, // passes :id as a prop to the component
},
{
path: 'calls',
children: [
{
path: '',
name: 'CallAnalytics',
component: () => import('@/pages/calls/CallAnalytics.vue'),
},
{
path: ':callId/transcript',
name: 'CallTranscript',
component: () => import('@/pages/calls/CallTranscript.vue'),
props: true,
},
],
},
{
path: 'admin',
name: 'Admin',
component: () => import('@/pages/Admin.vue'),
meta: { requiresAdmin: true },
},
],
},
// Catch-all 404
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/pages/NotFound.vue'),
},
]Typed Route Parameters
Define route types for TypeScript safety:
TYPESCRIPT
// src/types/router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
requiresAdmin?: boolean
title?: string
icon?: string
layout?: 'default' | 'blank'
}
}Access in components:
VUE
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// Typed — route.params.id is string
const appointmentId = route.params.id as string
// Or with props: true on the route
const props = defineProps<{ id: string }>()
// Navigate programmatically
function goToAppointment(id: string) {
router.push({ name: 'AppointmentDetail', params: { id } })
}
// With query params
function search(query: string) {
router.push({ name: 'Appointments', query: { q: query, page: '1' } })
}
// Replace (no browser history entry)
router.replace({ name: 'Dashboard' })
// Go back
router.back()
</script>Navigation Guards
Global Guard — Authentication
TYPESCRIPT
// src/router/index.ts
router.beforeEach(async (to, from) => {
const auth = useAuthStore()
// Initialize auth state on first load
if (!auth.user) {
await auth.initialize()
}
// Route requires auth, user not logged in
if (to.meta.requiresAuth !== false && !auth.isAuthenticated) {
return {
name: 'Login',
query: { redirect: to.fullPath }, // remember where they were going
}
}
// Route requires admin, user is not admin
if (to.meta.requiresAdmin && !auth.isAdmin) {
return { name: 'Dashboard' }
}
// Already logged in, trying to visit /login
if (to.name === 'Login' && auth.isAuthenticated) {
return { name: 'Dashboard' }
}
})Per-Route Guard
TYPESCRIPT
{
path: 'appointments/:id',
component: () => import('@/pages/AppointmentDetail.vue'),
beforeEnter: async (to) => {
// Validate the ID exists before rendering
const store = useAppointmentStore()
const appt = await store.fetchById(to.params.id as string)
if (!appt) return { name: 'NotFound' }
}
}In-Component Guard
VUE
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedChanges = ref(false)
// Warn user before leaving with unsaved changes
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('You have unsaved changes. Leave anyway?')
if (!confirmed) return false // cancel navigation
}
})
// When route params change on same component (e.g., /appointments/1 → /appointments/2)
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
await store.fetchById(to.params.id as string)
}
})
</script>Redirect After Login
VUE
<!-- src/pages/Login.vue -->
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
async function onSubmit() {
await auth.login(email.value, password.value)
// Redirect to the page they originally tried to visit
const redirect = route.query.redirect as string | undefined
router.push(redirect ?? { name: 'Dashboard' })
}
</script>Nested Layouts
VUE
<!-- src/layouts/AppLayout.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
import Sidebar from '@/components/Sidebar.vue'
import Header from '@/components/Header.vue'
const route = useRoute()
</script>
<template>
<div class="flex min-h-screen">
<Sidebar />
<div class="flex-1 flex flex-col">
<Header :title="route.meta.title" />
<main class="flex-1 p-6">
<!-- Child routes render here -->
<RouterView />
</main>
</div>
</div>
</template>Dynamic Page Titles
TYPESCRIPT
router.afterEach((to) => {
const base = 'Learnixo Portal'
const title = to.meta.title
document.title = title ? `${title} — ${base}` : base
})useRouter Composable for Navigation
TYPESCRIPT
// src/composables/useNavigation.ts
import { useRouter } from 'vue-router'
export function useNavigation() {
const router = useRouter()
return {
toAppointment: (id: string) => router.push({ name: 'AppointmentDetail', params: { id } }),
toCallTranscript: (callId: string) => router.push({ name: 'CallTranscript', params: { callId } }),
toDashboard: () => router.push({ name: 'Dashboard' }),
toLogin: (redirect?: string) => router.push({
name: 'Login',
query: redirect ? { redirect } : undefined,
}),
}
}Enjoyed this article?
Explore the Frontend Engineering learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.