skip to content

Migrating a blog's hand-written Vue Router routes array to Vue Router 5's src/pages file routing, what breaks first, and how do you move named routes, redirects and beforeEnter guards?

level: seniorimportance: should knowfreq 22%

answer

  1. rename files, not routes
  2. names now come from paths
  3. definePage keeps old names
  4. redirects go on the array
  5. guards move to meta

basics

~20 s

Route names change first: generated names like /posts/[slug] break every push by the old name. Keep names with definePage({ name }) or update callers, append redirects to the generated routes array, and turn beforeEnter guards into meta checked by a global guard.

solid answer

~40 s

Move each component to its file path (`Home.vue` to `index.vue`, `Post.vue` to `posts/[slug].vue`) and replace the array with `routes` from `vue-router/auto-routes`. The first thing that breaks is naming: generated names are file paths like `/posts/[slug]`, so every `push({ name: 'post' })` fails type-checking, which usefully lists every caller. Either restore the old names with `definePage({ name: 'post' })` or rename the callers. Redirect records have no page file, so push them onto the imported `routes` before `createRouter()`; they work but stay out of the types. `beforeEnter` is not supported in `definePage()`, so move each check into `meta` plus a global `beforeEach`. Teams coming from `unplugin-vue-router` mostly change import paths to `vue-router/vite`.

code

vue · 15 lines
vue
<!-- src/pages/posts/[slug].vue, formerly Post.vue -->
<script setup lang="ts">
import { useRoute } from 'vue-router'

definePage({
  name: 'post',                      // keep the old name for existing callers
  meta: { draftAccess: true },       // was a beforeEnter guard
})

const route = useRoute('post')
</script>

<template>
  <article>{{ route.params.slug }}</article>
</template>

go deeper

for a junior

Recall the renames: Home.vue to index.vue, Post.vue to posts/[slug].vue, and routes imported from vue-router/auto-routes.

for a middle

Explain why names change to file paths and the three homes for old config: definePage() for name, meta and alias, the array for redirects, a global guard for checks.

for a senior

Show a migration plan that uses the type errors as the checklist, keeps redirects out of the types on purpose and never pastes beforeEnter into definePage().

for a principal

Decide the naming scheme for the whole app, generated or custom via getRouteName, and stage the move so URLs stay stable for readers and search engines.

## The starting point A typical blog router before migration is a hand-written array: `{ path: '/', name: 'home', component: Home }`, `{ path: '/posts/:slug', name: 'post', component: Post, beforeEnter: requireDraftAccess }`, a redirect from the old `/blog/:slug` URLs, and a not-found record. File-based routing in Vue Router 5 replaces that array with one derived from `src/pages`, so migration is mostly moving files and then finding everything the array used to say that a file name cannot. ## Step by step 1. **Install the plugin.** Add `VueRouter()` from `vue-router/vite` before the Vue plugin, and set `dts: 'src/route-map.d.ts'` so the generated types sit inside `src/`. 2. **Move the components.** `Home.vue` becomes `src/pages/index.vue`, `Post.vue` becomes `src/pages/posts/[slug].vue`, and the not-found page becomes `src/pages/[...path].vue`. 3. **Swap the array.** Import `routes` (and `handleHotUpdate`) from `vue-router/auto-routes` and pass them to `createRouter()`. 4. **Run the type-check** and work through what it reports; the errors are the migration checklist. ## What breaks, and the fix for each | Old array feature | What happens after the move | Fix | |---|---|---| | `name: 'post'` | the route is now named `/posts/[slug]`; `push({ name: 'post' })` is a type error | `definePage({ name: 'post' })`, or update callers to the generated name | | redirect record | there is no page file for it | push `{ path: '/blog/:slug', redirect: ... }` onto `routes` before `createRouter()` | | `beforeEnter` | not supported in `definePage()` | `definePage({ meta: { ... } })` plus a global `beforeEach`, or add it at runtime | | `meta` | lost with the array | move it into `definePage()` or a `<route>` block | | extra URL for one page | no second file | `definePage({ alias: '/p/:slug' })` | **Names deserve a decision, not a reflex.** Restoring the old names with `definePage({ name })` keeps a large diff small and is one of the two fixes the migration guide lists for name errors; the other is switching callers to the generated names. Adopting the generated names makes the name and the file path say the same thing, and the `getRouteName` option lets you swap the naming scheme globally, for example to `getPascalCaseRouteName` exported from `vue-router/unplugin`. Mixing schemes in one app is the option to avoid. ## Redirects and guards at runtime The docs present runtime edits to the generated array as an escape hatch: ```ts import { routes } from 'vue-router/auto-routes' routes.push({ path: '/blog/:slug', redirect: to => `/posts/${to.params.slug}`, }) ``` That is a good fit for legacy redirects precisely because they never appear in the generated types: nobody should navigate to an old URL by name. Guards are different. A `beforeEnter` placed inside `definePage()` would look as if it could use the component's variables, which it cannot, so the docs rule it out. Put a flag such as `meta: { draftAccess: true }` in `definePage()` and let one global guard enforce it. ## Proving no URL was lost Type errors catch broken names, not broken URLs. Before deleting the old array, turn it into a test: 1. Export the list of old paths, with a sample value for each param (`/posts/hello-world`, `/blog/hello-world`). 2. For each one, call `router.resolve(path)` on the new router and assert that it matched a route other than the catch-all `/[...path]`. 3. For redirects, assert that the resolved record carries the redirect you pushed onto `routes`. Run it in CI until the migration branch merges; it is cheap and catches the renamed folder that the compiler cannot. ## Coming from unplugin-vue-router instead Teams that already used file routing on Vue Router 4 have a shorter path, because the conventions did not change: - replace `unplugin-vue-router/vite` with `vue-router/vite`, and use `vue-router/unplugin` for other bundlers; - delete the `unplugin-vue-router/client` types reference and move the generated file to `src/route-map.d.ts`; - switch the Volar plugins to `vue-router/volar/sfc-typed-router` and `vue-router/volar/sfc-route-blocks`; - remove the `unplugin-vue-router` dependency. Apps on Vue Router 4 without file routing have no breaking changes when upgrading to 5, apart from the IIFE build no longer bundling the devtools API; the file-routing move is a separate, optional project.

  • Why is a redirect a good candidate for a runtime push onto the routes array?
    A redirect has no page to render, so there is no file to hold it, and the docs allow editing the generated array before `createRouter()`. Such records are missing from the generated types, which suits legacy URLs: users still reach them by typing or following old links, while application code cannot navigate to them by name.
  • Would you keep the old route names or adopt the generated ones?
    For a first pass, keeping them with `definePage({ name })` keeps the diff about files, not callers. Long term, generated names tie each name to its file, so grepping a name finds the page. Pick one scheme for the app and apply it with `getRouteName` if you change it globally.

saying these in an interview costs you the question

  • File routing keeps the old route names from the deleted array automatically.
  • Redirects must become page files that call router.replace() in setup.
  • beforeEnter can be pasted into definePage() unchanged.
  • Upgrading a plain Vue Router 4 app to 5 forces a move to src/pages.
  • Records pushed onto routes at runtime are also typed.