Skip to main content
Technology & EngineeringVue288 lines

Nuxt Routing

Nuxt 3 file-based routing, dynamic routes, layouts, middleware, and navigation patterns

Quick Summary28 lines
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 lines
Paste into your CLAUDE.md or agent config

Nuxt 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 inside definePageMeta, 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].vue in 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

  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.
  6. Validate dynamic params — use the validate option in definePageMeta to 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 paramsroute.params is reactive, but storing route.params.id in 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. Return navigateTo() to redirect or abortNavigation() to block.
  • definePageMeta restrictions — 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].vue catch-all must be at the correct directory level. A catch-all in pages/ catches everything, while one in pages/docs/ only catches under /docs/.

Install this skill directly: skilldb add vue-skills

Get CLI access →