Nuxt Routing
Nuxt 3 file-based routing, dynamic routes, layouts, middleware, and navigation patterns
You are an expert in Nuxt 3 file-based routing, layouts, middleware, and navigation for building full-stack Vue applications.
## Key Points
- **Catch-All Route at the Wrong Level** — placing `[...slug].vue` in a subdirectory expecting it to catch all application routes, when it only catches routes under that subdirectory's URL prefix.
- **File-based routing** — files in `pages/` map to URL paths automatically
- **Dynamic routes** — bracket syntax `[param]` for dynamic segments, `[...slug]` for catch-all
- **Layouts** — wrapper components in `layouts/` that provide shared page chrome
- **Middleware** — functions that run before navigation to guard or redirect routes
- **`<NuxtPage>`** — the component that renders the matched page (replaces `<router-view>`)
- **`<NuxtLink>`** — auto-prefetching link component (replaces `<router-link>`)
1. **Use `<NuxtLink>` instead of `<a>`** — it enables client-side navigation and automatic prefetching on viewport entry.
2. **Keep pages thin** — pages should be wiring components together, not containing business logic. Extract logic into composables and UI into components.
3. **Use `definePageMeta` for route config** — it is statically analyzed at build time for middleware, layout, and validation.
4. **Order middleware explicitly** — middleware runs in the order listed in the `middleware` array. Prefix global middleware with numbers (`01.`, `02.`) to control order.
5. **Use `useFetch` or `useAsyncData` in pages** — they handle SSR data fetching, client hydration, and deduplication automatically.
## Quick Example
```ts
// middleware/01.logger.global.ts
// The .global suffix makes it run on every route
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigating from ${from.path} to ${to.path}`)
})
```skilldb get vue-skills/Nuxt RoutingFull skill: 288 linesNuxt 3 Routing and Layouts — Vue.js
You are an expert in Nuxt 3 file-based routing, layouts, middleware, and navigation for building full-stack Vue applications.
Core Philosophy
Nuxt 3 routing embraces convention over configuration: the file system is the routing table. This is not just a convenience shortcut — it is a design philosophy that makes route structure visible and predictable by mapping URL hierarchy directly to directory hierarchy. When a new developer opens the pages/ directory, they immediately understand every route in the application without reading a configuration file.
The layered architecture of pages, layouts, and middleware creates a separation of concerns that scales naturally. Pages own their content and data requirements; layouts own the structural chrome that wraps multiple pages; middleware owns the access control and navigation logic that runs before anything renders. Each layer has a single responsibility and a well-defined hook point. This layering means you never put auth checks inside page components or navigation chrome inside middleware — each concern lives where it belongs.
Nuxt routing is designed for progressive enhancement: server-side rendering handles the first paint, <NuxtLink> enables client-side navigation with automatic prefetching, and useFetch/useAsyncData handle data hydration transparently. The goal is that developers write straightforward page components and the framework handles the complexity of making them work across SSR and client navigation seamlessly.
Anti-Patterns
-
Fat Page Components — putting business logic, API calls, and complex UI directly inside page files. Pages should be thin wiring layers that compose components and invoke composables. Extract everything else.
-
Dynamic Variables Inside
definePageMeta— trying to use runtime variables, imports, or computed values insidedefinePageMeta, which is statically analyzed and compiled away at build time. Only literal values and inline functions work. -
Missing
<NuxtPage />in Parent Routes — creating nested route directories without including<NuxtPage />in the parent page component, causing child routes to silently not render with no error message. -
Using
<a>Tags Instead of<NuxtLink>— bypassing Nuxt's navigation system with plain anchor tags, which forces full page reloads, skips client-side routing, and loses automatic prefetching behavior. -
Catch-All Route at the Wrong Level — placing
[...slug].vuein a subdirectory expecting it to catch all application routes, when it only catches routes under that subdirectory's URL prefix.
Overview
Nuxt 3 uses file-based routing powered by Vue Router under the hood. Pages in the pages/ directory automatically become routes. Nuxt adds layouts, route middleware, route validation, and server-side rendering support on top of Vue Router's core capabilities.
Core Concepts
- File-based routing — files in
pages/map to URL paths automatically - Dynamic routes — bracket syntax
[param]for dynamic segments,[...slug]for catch-all - Layouts — wrapper components in
layouts/that provide shared page chrome - Middleware — functions that run before navigation to guard or redirect routes
<NuxtPage>— the component that renders the matched page (replaces<router-view>)<NuxtLink>— auto-prefetching link component (replaces<router-link>)
Implementation Patterns
File Structure to Routes
pages/
index.vue → /
about.vue → /about
blog/
index.vue → /blog
[slug].vue → /blog/:slug
users/
[id]/
index.vue → /users/:id
settings.vue → /users/:id/settings
[...slug].vue → catch-all 404
Dynamic Route Parameters
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
// Type-safe param access
const slug = route.params.slug as string
const { data: post } = await useFetch(`/api/posts/${slug}`)
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<div v-html="post.content" />
</article>
</template>
Catch-All 404 Page
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// route.params.slug is an array of path segments
// e.g., /foo/bar/baz → ['foo', 'bar', 'baz']
setResponseStatus(404)
</script>
<template>
<div>
<h1>Page Not Found</h1>
<p>The path /{{ ($route.params.slug as string[]).join('/') }} does not exist.</p>
<NuxtLink to="/">Go home</NuxtLink>
</div>
</template>
Layouts
<!-- layouts/default.vue -->
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<main class="flex-1">
<slot />
</main>
<AppFooter />
</div>
</template>
<!-- layouts/dashboard.vue -->
<template>
<div class="flex">
<DashboardSidebar />
<main class="flex-1 p-6">
<slot />
</main>
</div>
</template>
<!-- pages/dashboard/index.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'dashboard',
})
</script>
<template>
<h1>Dashboard</h1>
</template>
Programmatic Layout Switching
<script setup lang="ts">
const route = useRoute()
// Disable layout for a specific page
definePageMeta({
layout: false,
})
</script>
<template>
<NuxtLayout name="custom">
<h1>Content with conditional layout</h1>
</NuxtLayout>
</template>
Route Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/login', { redirectCode: 301 })
}
})
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAdmin } = useAuth()
if (!isAdmin.value) {
return abortNavigation(
createError({ statusCode: 403, message: 'Forbidden' })
)
}
})
<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'],
})
</script>
Global Middleware
// middleware/01.logger.global.ts
// The .global suffix makes it run on every route
export default defineNuxtRouteMiddleware((to, from) => {
console.log(`Navigating from ${from.path} to ${to.path}`)
})
Route Validation
<!-- pages/users/[id].vue -->
<script setup lang="ts">
definePageMeta({
validate: async (route) => {
// Only allow numeric IDs
return /^\d+$/.test(route.params.id as string)
},
})
</script>
Nested Routes
pages/
users/
[id].vue → parent (must contain <NuxtPage />)
[id]/
index.vue → /users/:id
profile.vue → /users/:id/profile
settings.vue → /users/:id/settings
<!-- pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const userId = route.params.id
</script>
<template>
<div>
<nav>
<NuxtLink :to="`/users/${userId}`">Overview</NuxtLink>
<NuxtLink :to="`/users/${userId}/profile`">Profile</NuxtLink>
<NuxtLink :to="`/users/${userId}/settings`">Settings</NuxtLink>
</nav>
<NuxtPage />
</div>
</template>
Programmatic Navigation
// In <script setup>
const router = useRouter()
// Navigate
await navigateTo('/dashboard')
// Navigate with query params
await navigateTo({ path: '/search', query: { q: 'vue' } })
// Replace current history entry
await navigateTo('/login', { replace: true })
// External URL
await navigateTo('https://example.com', { external: true })
Best Practices
- Use
<NuxtLink>instead of<a>— it enables client-side navigation and automatic prefetching on viewport entry. - Keep pages thin — pages should be wiring components together, not containing business logic. Extract logic into composables and UI into components.
- Use
definePageMetafor route config — it is statically analyzed at build time for middleware, layout, and validation. - Order middleware explicitly — middleware runs in the order listed in the
middlewarearray. Prefix global middleware with numbers (01.,02.) to control order. - Use
useFetchoruseAsyncDatain pages — they handle SSR data fetching, client hydration, and deduplication automatically. - Validate dynamic params — use the
validateoption indefinePageMetato return 404 early for invalid params.
Common Pitfalls
- Missing
<NuxtPage />in parent route — nested routes require the parent page to include<NuxtPage />or the child pages will not render. - Using
useRoute()reactively for params —route.paramsis reactive, but storingroute.params.idin a plain variable loses reactivity. Use a computed or access it directly in the template. - Middleware returning
undefined— if middleware does not explicitly return, navigation proceeds. ReturnnavigateTo()to redirect orabortNavigation()to block. definePageMetarestrictions — it is compiled away at build time, so you cannot use dynamic variables or imports inside it. Only static values and inline functions are allowed.- Catch-all route ordering — the
[...slug].vuecatch-all must be at the correct directory level. A catch-all inpages/catches everything, while one inpages/docs/only catches under/docs/.
Install this skill directly: skilldb add vue-skills
Related Skills
Composables
Building reusable custom composables in Vue 3 with proper typing, lifecycle management, and state sharing
Composition API
Patterns and best practices for Vue 3 Composition API including refs, reactive, computed, watchers, and lifecycle hooks
Nuxt Server
Nuxt 3 server routes, API endpoints, server middleware, and Nitro engine patterns for full-stack development
Pinia
Pinia state management for Vue 3 including store design, actions, getters, plugins, and SSR hydration
Provide Inject
Vue 3 provide/inject for dependency injection, deeply nested component communication, and plugin design
Vue Router
Vue Router 4 advanced patterns including navigation guards, lazy loading, scroll behavior, and typed routes