Skip to content
Release notesRouting

Vue Router 5: file-based routing moves into core

Vue Router 5.0.0 was published on 29 January. For an app with a routes array it is a version bump. For file-based routing it means one dependency fewer and a handful of new import paths.

vuejs.school team3 Feb 20267 min readVue Router 5.0

Vue Router is the official router for Vue: it maps each URL to the component that should render for it. Version 5.0 moves file-based routing, which used to live in a separate package called unplugin-vue-router, into vue-router itself. Apart from one change to the build that pages load from a CDN, nothing breaks. The release notes call it a "boring" release, and they mean it as a compliment. Two patches followed: 5.0.1 on 30 January and 5.0.2 on 2 February, so 5.0.2 is the one to install.

Apps on unplugin-vue-router drop that dependency and change some import paths. Two other groups should not just bump the version: Nuxt apps, which choose their router version themselves, and pages that load the CDN build with a script tag and use the devtools there.

What shipped

The release notes and the migration guide list these changes:

  • unplugin-vue-router is merged into vue-router. The Vite plugin is imported from vue-router/vite, the generated routes from vue-router/auto-routes, and the plugins for webpack, Rollup and esbuild, with their helper functions, from vue-router/unplugin.
  • Data loaders, a way to fetch a page's data as part of the navigation, ship as experimental under vue-router/experimental. A Pinia Colada flavour sits under vue-router/experimental/pinia-colada.
  • The Volar plugins move into the package as vue-router/volar/sfc-typed-router and vue-router/volar/sfc-route-blocks. Volar is the engine behind the Vue editor extension and vue-tsc, and these plugins teach it about your routes.
  • A JSON schema for the <route> custom block that a page component can contain.
  • The devtools integration moves to version 8 of @vue/devtools-api.
  • One bug fix you might notice: navigation guards now run when a component cached by <KeepAlive> is reactivated for a different route.

The two patches are small. 5.0.1 makes the typed-router Volar plugin work with vue-tsc, and 5.0.2 removes the devtools code from the IIFE build and loosens a version check.

Upgrade an app with a routes array

If you write your routes by hand in a routes array and pass it to createRouter(), the guide has one line for you: no breaking changes. Update the dependency and you are done:

pnpm update vue-router@5

With npm the command is npm install vue-router@5. The package still asks for Vue 3.5 or newer, the same peer range as 4.6, so any project that runs 4.6 can install 5.

There is one exception, and most apps will never meet it. The IIFE build, the single file you load from a CDN with a plain <script> tag that puts a global VueRouter on the page, no longer bundles @vue/devtools-api. Version 8 of that package does not publish an IIFE build of its own, so the router cannot include it. Projects built with Vite use the ES module build and are not affected.

Set up file-based routing from the core package

File-based routing means the folder of page components is the route table. Add src/pages/about.vue and the URL /about exists, with no array to edit. Before 5.0 you installed unplugin-vue-router for this. Now the Vite plugin comes from the package you already have:

vite.config.ts
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',
    }),
    // VueRouter() must come before vue()
    vue(),
  ],
})

The order matters. VueRouter() reads and transforms page components before the Vue plugin compiles them, so it has to be listed first.

The dts option names the file where the plugin writes the types for your routes. The migration guide recommends src/route-map.d.ts because most setups already type-check everything under src/, and a create-vue project's tsconfig.app.json does. Leave the option out and the plugin writes typed-router.d.ts at the project root, which you then have to add to include yourself.

The plugin reads src/pages by default and turns each file name into a path:

  • src/pages/index.vue renders at /.
  • src/pages/about.vue renders at /about.
  • src/pages/users/index.vue renders at /users.
  • src/pages/users/[id].vue renders at /users/:id, and id becomes a route param.
  • src/pages/[...path].vue matches every URL nothing else matched, which is where a 404 page goes.

The router file stops listing routes and imports them instead:

src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { routes, handleHotUpdate } from 'vue-router/auto-routes'

export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

if (import.meta.hot) {
  handleHotUpdate(router)
}

handleHotUpdate lets the dev server swap the routes when you add or rename a page, without a full reload.

The other conventions, such as nested routes, folders in parentheses that group pages without changing the URL, and named views, are documented on the unplugin-vue-router site. That is still where the file-based guide lives for 5.0; the router docs link to it.

Typed routes and params

The plugin also writes a route map: every route name, its path and its params. A route's name is its file path inside src/pages, so the user page above is called /users/[id]. Pass that name to useRoute() and its params are typed:

src/pages/users/[id].vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute('/users/[id]')
const nextId = computed(() => Number(route.params.id) + 1)
</script>

<template>
  <h1>User {{ route.params.id }}</h1>
  <RouterLink :to="{ name: '/users/[id]', params: { id: nextId } }">
    Next user
  </RouterLink>
</template>

What TypeScript now checks in this page:

  • route.params.id is a string, and reading any other param, such as route.params.slug, is an error.
  • The RouterLink's to is checked against the same map, so a typo such as name: '/user/[id]' fails vue-tsc instead of failing in the browser.
  • Params you pass in accept numbers as well as strings, which is why nextId can be passed as it is.

Commit the generated file. Its own header recommends it, and a fresh clone then type-checks before anyone has started the dev server.

Repeating the route name in every page gets old. The Volar plugin types useRoute() and $route in page components from the file path, so a bare useRoute() is enough. Add it, with rootDir, to the tsconfig that includes your Vue files (tsconfig.app.json in a create-vue project):

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

Merge these keys into your existing file rather than replacing it. The plugin works out each page's path from rootDir. Without it, vue-tsc warns "No rootDir specified" and a bare useRoute() does not get the page's params.

Data loaders, still experimental

Data loaders move fetching out of the component. You define a loader with defineBasicLoader() and export it from a page component. The router runs it during navigation and waits for it before showing the page, and the page reads data, isLoading, error and reload from it. Install the plugin in src/main.ts, before the router, because adding the router starts the first navigation. The router here is the named export from src/router/index.ts above:

src/main.ts
import { createApp } from 'vue'
import { DataLoaderPlugin } from 'vue-router/experimental'
import App from './App.vue'
import { router } from './router'

const app = createApp(App)
app.use(DataLoaderPlugin, { router })
app.use(router)
app.mount('#app')

Try data loaders in a side project. In an app you ship, keep fetching inside your components or your data layer until the API leaves the experimental path.

Version 6 is planned to be ESM only

The migration guide states the plan in two sentences: Vue Router 6 will be ESM only and will remove deprecated APIs, and version 5 exists to give you time to prepare. ESM only means the package will stop shipping a CommonJS build, the one loaded with require('vue-router').

A Vite app is already ESM. The places to check are an old test setup, a Node script or a server entry that still uses require. The guide does not yet list which APIs will be removed, so there is nothing more to change today.

Who should upgrade now, and who should wait

  • An app with a routes array: upgrade now. It is one command, the guide promises no code changes, and staying on the current major means every fix lands where you are.
  • An app on unplugin-vue-router: upgrade now as well. The guide calls the move "mostly import path changes": remove the old dependency, update vue-router, and point each import at its new path. Change the imports in one commit, run vue-tsc and the dev server, and you are done in an afternoon.
  • A new project: nothing to do. When you pick the router option, create-vue 3.21.0, published on 2 February, installs vue-router 5 and still writes a classic routes array in src/router/index.ts. File-based routing stays opt-in.
  • A Nuxt app: leave the router to Nuxt. Nuxt has its own pages integration and chooses the router version itself (Nuxt 4.3 depends on vue-router ^4.6.4), so do not add version 5 by hand.
  • A page that loads the router from a CDN: wait if you rely on the devtools there, because the IIFE build no longer includes them.
Check it stuck3 questions from the File-based routing with Vue Router 5 quiz.
Practice now
Was this helpful?

Keep reading