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?
answer
- rename files, not routes
- names now come from paths
- definePage keeps old names
- redirects go on the array
- guards move to meta
basics
~20 sRoute 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 sMove 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<!-- 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
Recall the renames: Home.vue to index.vue, Post.vue to posts/[slug].vue, and routes imported from vue-router/auto-routes.
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.
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().
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.