---
name: sdlc-vue-routing
description: |
Vue Router v4 (Vue 3 default) — route configuration, nested routes, dynamic segments, navigation guards, lazy loading, typed routes, route meta, programmatic navigation. Vue Router v3 (Vue 2) pointer only.
Use this skill to:
- Configure routes with createRouter + createWebHistory.
- Use useRoute / useRouter composables in components.
- Lazy-load route components for code splitting.
- Implement auth guards via meta + beforeEach.
- Type route params for safer access.
Do NOT use this skill for:
- General Vue conventions (see vue-conventions).
- State management (see vue-state-management).
- Forms (see vue-forms).
- Testing routes (see vue-testing).
paths: ["src/**/*.vue", "src/**/*.ts"]
---
# Vue Router v4 Patterns
The de-facto router for Vue 3. Use whatever the project has installed; don't introduce a new router lib.
## Setup
```ts
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'home', component: HomeView },
{ path: '/about', name: 'about', component: () => import('@/views/AboutView.vue') }, // lazy
{ path: '/users', name: 'users', component: () => import('@/views/UsersView.vue') },
{ path: '/users/:id', name: 'user-detail', component: () => import('@/views/UserDetailView.vue'), props: true },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue') },
],
});
export default router;
```
```ts
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
```
```vue
```
## History modes
- `createWebHistory()` — HTML5 History API (`/about`, `/users/123`). Requires server config to fall back to `index.html` for unmatched routes.
- `createWebHashHistory()` — hash-based (`/#/about`). No server config needed; less SEO-friendly.
- `createMemoryHistory()` — for SSR/testing.
Default to `createWebHistory()` unless deploying to static host without fallback.
## Route definitions
```ts
const routes = [
// Static
{ path: '/', name: 'home', component: HomeView },
// Dynamic segment
{ path: '/users/:id', name: 'user', component: UserView, props: true },
// Multiple dynamic segments
{ path: '/posts/:category/:slug', component: PostView },
// Optional segment (Vue Router v4: use route alias or two routes)
{ path: '/users/:id/:tab?', component: UserView }, // ? means optional in v4
// Catch-all
{ path: '/:pathMatch(.*)*', component: NotFoundView },
// Aliases (multiple URLs → one component)
{ path: '/home', component: HomeView, alias: '/' },
// Redirects
{ path: '/old-path', redirect: '/new-path' },
{ path: '/old-user/:id', redirect: (to) => ({ path: '/users/' + to.params.id }) },
];
```
### Nested routes
```ts
{
path: '/dashboard',
component: DashboardLayout,
children: [
{ path: '', component: DashboardHome }, // /dashboard
{ path: 'analytics', component: Analytics }, // /dashboard/analytics
{ path: 'settings', component: Settings }, // /dashboard/settings
],
}
```
`DashboardLayout.vue` includes `` to render children:
```vue
```
## Navigation hooks
### Composables
```vue
```
`router.push`, `router.replace`, `router.back()`, `router.forward()`, `router.go(n)`.
### Push variants
```ts
router.push('/users/123'); // string path
router.push({ path: '/users/123' }); // object with path
router.push({ name: 'user', params: { id: '123' } }); // named route
router.push({ name: 'user', params: { id: '123' }, query: { tab: 'overview' } });
router.push({ path: '/users/123', hash: '#bio' });
```
Named routes are safer — refactoring the URL doesn't break the call.
## ``
```vue
Users
Alice
Users
Log in
```
For external links: plain ``. `` is for internal navigation only.
## Lazy loading
```ts
{
path: '/users',
name: 'users',
component: () => import('@/views/UsersView.vue'), // dynamic import → code split
}
```
Vite/Webpack handle code splitting automatically. Each lazy-loaded route becomes a separate chunk.
For grouped chunks:
```ts
component: () => import(/* webpackChunkName: "users" */ '@/views/UsersView.vue')
```
## Navigation guards
### Global
```ts
router.beforeEach(async (to, from) => {
// returning false → cancel navigation
// returning a route object → redirect
// returning undefined or true → continue
if (to.meta.requiresAuth && !await isAuthenticated()) {
return { name: 'login', query: { redirect: to.fullPath } };
}
});
router.afterEach((to, from, failure) => {
// analytics, scroll restoration, etc.
});
```
### Per-route
```ts
{
path: '/admin',
component: AdminView,
beforeEnter: (to) => {
if (!isAdmin()) return { name: 'home' };
},
}
```
### Per-component
```vue
```
### Auth guard pattern
```ts
const routes = [
{ path: '/login', name: 'login', component: LoginView, meta: { requiresAuth: false } },
{ path: '/dashboard', name: 'dashboard', component: DashboardView, meta: { requiresAuth: true } },
{ path: '/admin', name: 'admin', component: AdminView, meta: { requiresAuth: true, requiresAdmin: true } },
];
router.beforeEach(async (to) => {
const userStore = useUserStore();
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } };
}
if (to.meta.requiresAdmin && !userStore.isAdmin) {
return { name: 'dashboard' };
}
});
```
## Typed routes
Vue Router v4 doesn't auto-type params. Two approaches:
### Manual TypeScript typing
```ts
// src/router/types.ts
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
requiresAdmin?: boolean;
title?: string;
}
}
// Per-route typing in components
const route = useRoute();
const userId = route.params.id as string; // cast — type-safe-ish
```
### `unplugin-vue-router` (file-based, generates types)
```bash
pnpm add -D unplugin-vue-router
```
`vite.config.ts`:
```ts
import VueRouter from 'unplugin-vue-router/vite';
export default defineConfig({
plugins: [VueRouter({ routesFolder: 'src/pages' }), vue()],
});
```
Place files in `src/pages/`:
```
pages/
├── index.vue # /
├── about.vue # /about
├── users/
│ ├── index.vue # /users
│ └── [id].vue # /users/:id
└── [...path].vue # catch-all
```
Generated types in `typed-router.d.ts` provide compile-time safety:
```ts
const route = useRoute('/users/[id]');
const userId = route.params.id; // typed as string
```
## Route meta for cross-cutting concerns
```ts
{
path: '/dashboard',
component: DashboardView,
meta: {
requiresAuth: true,
title: 'Dashboard',
layout: 'app',
},
}
```
Read in `App.vue` for dynamic page titles:
```vue
```
## Scroll behavior
```ts
const router = createRouter({
history: createWebHistory(),
routes: [...],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition; // back/forward — restore position
if (to.hash) return { el: to.hash, behavior: 'smooth' };
return { top: 0 }; // new route — scroll to top
},
});
```
## Vue Router v3 (Vue 2 legacy — pointer)
```ts
import VueRouter from 'vue-router';
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/users/:id', component: UserView },
],
});
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuth()) next('/login');
else next();
});
```
Differences:
- `new VueRouter()` instead of `createRouter`.
- `mode: 'history' | 'hash'` instead of explicit history factory.
- Guards take `next` callback instead of returning a route.
- `useRoute` / `useRouter` composables don't exist; use `this.$route` / `this.$router`.
Migrating v3 → v4 is invasive (entire route file rewrites). Don't migrate as part of feature work.
## Anti-patterns
- ❌ Forgetting `` in App.vue or layouts — routes don't render.
- ❌ `` for internal links — full page reload, defeats SPA.
- ❌ Storing route state in component when query params suffice — duplicate truth.
- ❌ Heavy logic in `beforeEach` that runs on every nav — cache where possible.
- ❌ Mixing string paths and named routes inconsistently — pick one.
- ❌ Forgetting to handle navigation failures (e.g., guard returns false) — async errors silently dropped.
- ❌ Accessing `route.params.id` without type cast or schema validation — bare `any`-like access.
- ❌ Redirects in `beforeEach` without checking `to.name === 'login'` — infinite loop on guard.
- ❌ Long routes file — split by feature into multiple route arrays, then concat.