The silent cost of broken links in production
We've all been there: a route name changes, a parameter shifts, and suddenly your navigation is silently failing in production. Users click a link that should work, but nothing happens. The browser doesn't throw an error—it just silently refuses to navigate. Typed routes: what every Vue & Nuxt developer should know is about catching these breaks before they reach your users, at compile time, with full IDE autocomplete and type safety throughout your application.
In this article, we'll walk through how to set up typed route definitions, why they matter in larger codebases, and how Nuxt's built-in system compares to the file-based approach used in standalone Vue projects.
The problem: string-based route navigation
Most Vue projects start simple. You define routes as strings:
// router/index.ts
const routes = [
{ path: '/users/:id', name: 'user-detail', component: UserDetail },
{ path: '/dashboard', name: 'dashboard', component: Dashboard }
]
// In a component
const router = useRouter()
router.push({ name: 'user-detail', params: { id: 42 } })This works until it doesn't. Someone renames 'user-detail' to 'userDetail' and forgets to update the push call. TypeScript won't catch it. The route definition changes but your navigation code doesn't, and the link breaks in production.
Even worse: if you misspell a param name—{ userId: 42 } instead of { id: 42 }—there's no compile-time warning. You discover it when you test manually, or worse, when a user reports it.
unplugin-vue-router for file-based typed routes
For standalone Vue projects, unplugin-vue-router solves this by generating type-safe route definitions directly from your file system structure. Instead of manually maintaining a routes array, you define routes by folder layout, and the plugin generates fully typed route names and params.
Setting up unplugin-vue-router
Install the package:
npm install -D unplugin-vue-router
npm install vue-routerConfigure it in your build tool. For Vite:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueRouter from 'unplugin-vue-router/vite'
export default defineConfig({
plugins: [
VueRouter({
dts: './src/typed-router.d.ts'
}),
vue()
]
})Now organize your pages folder with file-based routing:
src/pages/
index.vue // route: /
users/
index.vue // route: /users
[id].vue // route: /users/:id
dashboard.vue // route: /dashboard
admin/
settings.vue // route: /admin/settingsThe plugin automatically generates a typed router with all route names and params inferred from your folder structure. You can then import and use it:
import { useRouter } from 'vue-router'
export default defineComponent({
setup() {
const router = useRouter()
// This is now type-safe
router.push({
name: 'users-id', // autocomplete suggests this
params: { id: 42 } // TypeScript knows 'id' is a string param
})
}
})If you try to use a non-existent route name or pass the wrong param type, TypeScript will error before you compile.
Real-world scenario: a user management feature
Imagine you're building a user management dashboard. You have pages for listing users, viewing details, and editing. Without typed routes:
// Scattered throughout your app, no type checking
router.push({ name: 'user-detail', params: { userId: 42 } })
router.push({ name: 'users', query: { page: 1 } })
router.push({ name: 'user-edit', params: { id: 42 } })Did you notice the inconsistency? One uses userId, another uses id. With unplugin-vue-router, your folder structure is the source of truth:
src/pages/
users/
index.vue // /users
[id]/
index.vue // /users/:id
edit.vue // /users/:id/editNow the router knows:
// TypeScript enforces correct param names
router.push({ name: 'users-id', params: { id: 42 } })
router.push({ name: 'users-id-edit', params: { id: 42 } })Try to use userId instead of id, and you'll get a compile error. Every navigation is guaranteed to work.
Typed params and names
The real power of typed routes is that your route parameters are validated at every point in your code.
How types flow through your application
When you define a route with a param in your file structure:
// src/pages/posts/[slug].vue
// This creates a param 'slug' of type stringThe generated types enforce that this param must be provided when navigating to this route. If you define a route without the param, TypeScript catches it:
// Compiles fine
router.push({ name: 'posts-slug', params: { slug: 'my-post' } })
// TypeScript error: param 'slug' is missing
router.push({ name: 'posts-slug' })
// TypeScript error: unknown param 'id'
router.push({ name: 'posts-slug', params: { id: 42 } })You can also extract route types for use in your components. The generated type file gives you access to RouteNamedLocationTyped and other utilities:
import type { RouteNamedLocationTyped } from 'vue-router'
// Define your navigation function with full type safety
function navigateToPost(slug: string) {
const location: RouteNamedLocationTyped<'posts-slug'> = {
name: 'posts-slug',
params: { slug }
}
router.push(location)
}In larger teams, this becomes invaluable. A junior dev cannot accidentally break navigation because TypeScript simply won't allow it.
Autocomplete in IDEs
Type safety without autocomplete is only half the battle. A good typed router gives you full IDE support, suggesting valid routes and params as you type.
What autocomplete looks like in practice
In VS Code or WebStorm, when you write a navigation call:
router.push({ name: '' }) // Cursor here triggers autocompleteYour IDE will suggest:
'users-index''users-id''users-id-edit''dashboard''admin-settings'
Pick one, and the IDE then knows which params are required. When you type:
router.push({ name: 'users-id', params: { } }) // Cursor hereThe IDE suggests id as a required property. This feedback loop makes development faster and catches mistakes immediately, not in code review or production.
The same applies to the <router-link> component:
<router-link :to="{ name: '', params: {} }">Full autocomplete in template land, where many bugs hide.
Nuxt has typed routing built in
If you're using Nuxt, you don't need an external plugin. Nuxt 3+ includes typed file-based routing out of the box, and the experience is even more streamlined.
Nuxt's native typed routes
Nuxt generates types automatically when you use definePageMeta or structure your pages folder:
src/pages/
index.vue
products/
index.vue
[id].vue
about.vueIn any Nuxt component, use navigateTo with full type safety:
// app.vue
<script setup lang="ts">
// TypeScript knows all valid routes and their params
await navigateTo({
name: 'products-id',
params: { id: '123' }
})
</script>Nuxt also generates a .nuxt/types/router.d.ts` file that you can inspect to see exactly what types are available. If a route doesn't exist or a param is missing, you'll see red squiggles immediately.
Nuxt's RouteLocationResolved and middleware integration
Nuxt extends typed routes into middleware and hooks:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 'to' and 'from' are fully typed
// You can safely access to.name, to.params, etc.
if (to.name === 'admin-settings') {
if (!isAdmin()) {
return navigateTo('/login')
}
}
})This means your route guards and middleware logic is also type-checked. A typo in a route name will be caught at build time.
Query params and dynamic routes in Nuxt
Nuxt typed routes also handle optional segments and query params correctly. Define a dynamic catch-all route:
src/pages/
docs/
[...slug].vue // Matches /docs/any/nested/pathThe generated types know slug is an array:
navigateTo({
name: 'docs-slug',
params: { slug: ['guide', 'getting-started'] }
})Common mistakes
Mistake 1: Manually defining routes alongside file-based routes. If you mix pages/ folder routes with manually declared routes in a router.ts file, the type generation won't catch the manual ones. Stick to one approach per project. In Nuxt, always use pages folders. In Vue with unplugin-vue-router, rely on file structure, not manual config.
Mistake 2: Forgetting to regenerate types after moving files. If you rename or move a page file but your build hasn't run yet, old types may still be in scope. Always run npm run build or npm run dev after restructuring routes. The generated type file will refresh and catch stale references.
Mistake 3: Passing params as query when they should be path params. If your route is /users/[id].vue, the id must be a param, not a query string. Some developers accidentally write params: {} query: { id: 42 }. TypeScript enforces the correct structure, but be aware of the distinction.
Mistake 4: Ignoring generated type files in git. Some teams ignore the generated types file. Don't. Commit it so everyone on the team gets the same type definitions, and diffs reveal when routes change.
Key takeaways
- Typed routes catch broken navigation at compile time, not in production. Every route name and param is validated before you ship code.
- unplugin-vue-router brings file-based typed routing to standalone Vue projects, generating types from your pages folder structure.
- Nuxt 3+ includes typed file-based routing natively, making it the default experience without extra setup.
- IDE autocomplete for route names and params makes development faster and prevents typos that would otherwise hide until runtime.
- Route types flow through your entire application: navigation calls, middleware, guards, and component props are all type-checked.
- Consistency matters: commit your generated type files, use only file-based routes, and rebuild after structural changes.






Comments
Loading comments…