Skip to content
TutorialRouting

Migrating from unplugin-vue-router to Vue Router 5

Vue Router 5 contains everything unplugin-vue-router did. Moving over is one dependency fewer and a short list of import paths, and your page files do not change.

vuejs.school team5 Feb 20265 min readVue Router 5.0

Your Vite app generates its routes from a src/pages folder with unplugin-vue-router. Vue Router 5.0.0, published on 29 January, merged that plugin into the router itself, so the extra package can go. The migration guide calls the work "mostly import path changes", and that is accurate: set aside half an hour, follow the five steps below, and your page files stay exactly as they are.

This is for apps that add the plugin to their own Vite config. A Nuxt app has its own pages integration and picks the router version itself; Nuxt 4.3 still depends on Vue Router 4.6, so there is nothing to migrate there.

Step 1: swap the dependencies

Start from a clean Git working tree, so every change shows up in one diff, and check that the app builds. If the build was already failing, you will not know which errors are yours. Then remove the plugin and move the router to the new major:

pnpm remove unplugin-vue-router
pnpm update vue-router@5

With npm, run npm uninstall unplugin-vue-router and then npm install vue-router@5. Either way you get 5.0.2, the current patch, which includes the 5.0.1 fix that makes the typed-router Volar plugin work with vue-tsc (step 4 uses it). Vue itself does not need to move: Vue Router 5 asks for Vue 3.5 or newer, the same as 4.6.

If you also use data loaders with Pinia Colada, check that version too. The 5.0.2 package declares @pinia/colada 0.21.2 or newer as an optional peer dependency, so your package manager reports a peer dependency conflict if yours is older.

Nothing will build right now. That is expected: the config still imports a package that is gone.

Step 2: change the plugin import

The Vite plugin now comes from vue-router/vite. While you are in the file, add the dts option the guide recommends, which moves the generated types into src/:

vite.config.tsBreaks
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'

export default defineConfig({
  plugins: [VueRouter(), vue()],
})
vite.config.tsWorks
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueRouter from 'vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      dts: 'src/route-map.d.ts',
    }),
    vue(),
  ],
})

Keep VueRouter() before vue(), as before. Any other options you passed, such as routesFolder or extensions, stay exactly as they were. The Options type you may have imported to type them now comes from vue-router/unplugin.

Projects built with webpack, Rollup or esbuild import from vue-router/unplugin instead and call VueRouter.webpack(), VueRouter.rollup() and so on. The same entry exports the helpers that custom integrations use: VueRouterAutoImports, EditableTreeNode, createTreeNodeValue, createRoutesContext, getFileBasedRouteName and getPascalCaseRouteName. If you feed VueRouterAutoImports to unplugin-auto-import, change only that import line.

Step 3: move the generated types

unplugin-vue-router wrote its route map to typed-router.d.ts at the project root, and its setup had you include that file in tsconfig.json and add a reference to unplugin-vue-router/client. Remove both. In a create-vue project the reference sits in env.d.ts:

env.d.tsBreaks
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />
env.d.tsWorks
/// <reference types="vite/client" />

If you put the types in tsconfig.json instead, remove "./typed-router.d.ts" from include, and "unplugin-vue-router/client" from wherever you listed it. With dts: 'src/route-map.d.ts' you add nothing back: create-vue's tsconfig.app.json already includes src/**/*, which is the reason the guide suggests that path. If your tsconfig does not include src, add the new file to its include.

Then start the dev server once, so the plugin writes src/route-map.d.ts, and delete the old typed-router.d.ts. Nothing regenerates it any more, so it would only go stale. Commit the new file in its place.

Step 4: rename the Volar plugins

Skip this step if no tsconfig in your project lists a plugin starting with unplugin-vue-router/volar/. Volar is the language-tools framework behind the Vue editor extension and vue-tsc, and unplugin-vue-router shipped two plugins for it: sfc-typed-router types useRoute() in a page from the page's file path, and sfc-route-blocks gives <route> blocks editor support. Change their prefix in the tsconfig that includes your Vue files:

tsconfig.app.json
{
  "compilerOptions": {
    "rootDir": "."
  },
  "vueCompilerOptions": {
    "plugins": [
      "vue-router/volar/sfc-typed-router",
      "vue-router/volar/sfc-route-blocks"
    ]
  }
}

Before, both strings started with unplugin-vue-router/volar/. Keep "rootDir": ".", which the plugins need to resolve paths, and merge these keys into your file rather than replacing it.

Step 5: update the data loader imports

Data loaders load a page's data inside a navigation guard, so the data is ready when the page renders. They were experimental in the plugin, they are still experimental, and their new address says so. Three old paths collapse into two. In src/main.ts only the import changes; app.use(DataLoaderPlugin, { router }) stays as it was:

src/main.ts
// before: import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders'
import { DataLoaderPlugin } from 'vue-router/experimental'

In a page, the loader sits in a plain <script> block next to <script setup>, because the router collects a page's loaders from its exports, and <script setup> cannot contain exports. Again, only the import line changes:

src/pages/users/[id].vue
<script lang="ts">
// before: import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic'
import { defineBasicLoader } from 'vue-router/experimental'

export const useUser = defineBasicLoader('/users/[id]', async (route) => {
  const res = await fetch(`/api/users/${route.params.id}`)
  return (await res.json()) as { id: string; name: string }
})
</script>

<script setup lang="ts">
const { data: user, isLoading, error } = useUser()
</script>

<template>
  <p v-if="isLoading">Loading…</p>
  <p v-else-if="error">{{ error.message }}</p>
  <h1 v-else-if="user">{{ user.name }}</h1>
</template>

defineColadaLoader moves from unplugin-vue-router/data-loaders/pinia-colada to vue-router/experimental/pinia-colada.

What stays the same

Your router file imports routes (and handleHotUpdate, if you set up hot reloading) from vue-router/auto-routes. unplugin-vue-router has used that module name since version 0.8, when it replaced vue-router/auto/routes, and Vue Router 5 keeps it: the package now carries the module's types, and its Vite plugin generates the routes behind it, as the old plugin did. The line stays as it is.

Page files do not move or change. Route names, which are file paths such as /users/[id], are generated the same way, so every useRoute('/users/[id]') and every named RouterLink keeps its types.

definePage() needs no import. It is a compiler macro, and in 5.0 the types behind vue-router/auto-routes declare it as a global. The guide does not mention this case, but if a file imports it explicitly from unplugin-vue-router/runtime, delete that import line.

Errors after migrating, and the fix for each

Run the dev server, then vue-tsc --build (create-vue's type-check script). If a step was missed, you will see one of these.

failed to load config from vite.config.ts, followed by Cannot find package 'unplugin-vue-router'. The config still imports the old plugin. Go back to step 2.

Cannot find type definition file for 'unplugin-vue-router/client'. A reference survived in env.d.ts, or in the types array of a tsconfig. Delete it.

Failed to resolve import "unplugin-vue-router/data-loaders/basic" from "src/pages/users/[id].vue". Does the file exist? A page still imports a loader from the old path. Search the src/pages folder for it and apply step 5.

Route types stop working: useRoute('/users/[id]') complains, or route.params falls back to a loose type. The guide's advice is to restart the TypeScript server in your editor and check that the generated file, src/route-map.d.ts, is included by your tsconfig.

The routes array is empty and every URL renders nothing. The plugin is not finding pages. Check routesFolder and the file extensions you allow; the default is src/pages with .vue files.

A route name is reported as unknown. Use the generated name, or give the page a name of your own with definePage({ name: 'user-profile' }) and use that.

ESLint reports 'definePage' is not defined in a JavaScript project. The no-undef rule does not know about compiler macros. The unplugin-vue-router docs give the fix, and it still applies: declare definePage as a readonly global in your ESLint config. TypeScript projects rarely see this, because the TypeScript presets switch no-undef off.

Check it stuck3 questions from the File-based routing with Vue Router 5 quiz.
Practice now
Was this helpful?

Keep reading