File-Based Routing
Nuxt 3 eliminates manual router configuration by using the file system itself as the routing definition. Every .vue file inside the pages/ directory automatically becomes a route. This convention removes the need for a separate router config file, reduces boilerplate, and makes the URL structure of an application immediately visible from the project tree.
The pages/ Directory
When Nuxt detects a pages/ directory, it auto-imports vue-router and generates the route table at build time. The file path maps directly to the URL path.
pages/
index.vue -> /
about.vue -> /about
contact.vue -> /contact
blog/
index.vue -> /blog
archive.vue -> /blog/archive
A minimal page component requires no special setup:
<script setup lang="ts">
// This page is automatically registered as /about
</script>
<template>
<div>
<h1>About Us</h1>
<p>This page is served at /about with zero router config.</p>
</div>
</template>
If the pages/ directory does not exist, Nuxt skips vue-router entirely, which is useful for single-page apps that handle their own rendering (such as a landing page with no navigation).
Dynamic Routes
Dynamic segments are defined by wrapping a portion of the filename in square brackets. The bracketed name becomes a route parameter accessible via useRoute().
Single Parameter
pages/
users/
[id].vue -> /users/:id
<script setup lang="ts">
const route = useRoute()
// /users/42 -> route.params.id === '42'
const userId = route.params.id
</script>
<template>
<div>
<h1>User Profile: {{ userId }}</h1>
</div>
</template>
Multiple Parameters
Multiple dynamic segments can appear at different levels:
pages/
projects/
[projectId]/
tasks/
[taskId].vue -> /projects/:projectId/tasks/:taskId
<script setup lang="ts">
const route = useRoute()
const projectId = route.params.projectId
const taskId = route.params.taskId
</script>
<template>
<div>
<h2>Project {{ projectId }} - Task {{ taskId }}</h2>
</div>
</template>
Catch-All Routes
The [...slug] syntax matches any number of path segments. This is useful for CMS pages, documentation sites, or any URL structure that is not known at build time.
pages/
docs/
[...slug].vue -> /docs/*, e.g. /docs/getting-started/installation
<script setup lang="ts">
const route = useRoute()
// /docs/getting-started/installation -> slug === ['getting-started', 'installation']
const segments = route.params.slug as string[]
const fullPath = segments.join('/')
</script>
<template>
<div>
<h1>Documentation</h1>
<p>Viewing: {{ fullPath }}</p>
</div>
</template>
If no match is needed for the bare /docs path, add a separate pages/docs/index.vue.
Nested Routes
Nuxt supports nested routes through directory nesting combined with the <NuxtPage /> component. When a directory and a same-named .vue file exist side by side, the file becomes the parent layout and the directory contents render inside it.
pages/
users.vue -> parent wrapper for /users/*
users/
index.vue -> /users
[id].vue -> /users/:id
[id]/
settings.vue -> /users/:id/settings
billing.vue -> /users/:id/billing
The parent file must include <NuxtPage /> as the outlet for child routes:
<!-- pages/users.vue -->
<script setup lang="ts">
// Shared logic for all /users/* routes
</script>
<template>
<div>
<nav>
<NuxtLink to="/users">All Users</NuxtLink>
</nav>
<NuxtPage />
</div>
</template>
<!-- pages/users/[id]/settings.vue -->
<script setup lang="ts">
const route = useRoute()
</script>
<template>
<div>
<h2>Settings for User {{ route.params.id }}</h2>
<form>
<label>Display Name <input type="text" /></label>
</form>
</div>
</template>
Client-Side Navigation with NuxtLink
NuxtLink is Nuxt's replacement for both <a> tags and <RouterLink>. It provides client-side navigation (no full page reload), automatic prefetching of linked pages when they enter the viewport, and correct handling of both internal and external URLs.
<template>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink :to="{ name: 'users-id', params: { id: 7 } }">User 7</NuxtLink>
<NuxtLink to="https://vuejs.org" external>Vue Docs</NuxtLink>
</nav>
</template>
NuxtLink automatically adds the router-link-active and router-link-exact-active CSS classes, which makes styling active navigation items straightforward:
<style scoped>
.router-link-active {
font-weight: bold;
color: #10b981;
}
</style>
Route Middleware
Middleware functions run before a page renders. They are defined using defineNuxtRouteMiddleware and applied to pages via definePageMeta. Middleware is the standard mechanism for route guards such as authentication checks.
Inline Middleware
The simplest approach attaches middleware directly inside a page:
<script setup lang="ts">
definePageMeta({
middleware: [
function (to, from) {
const isAuthenticated = Boolean(useCookie('session').value)
if (!isAuthenticated) {
return navigateTo('/login')
}
}
]
})
</script>
<template>
<div>
<h1>Dashboard</h1>
<p>Only visible to authenticated users.</p>
</div>
</template>
Named Middleware
For reusable guards, create a file in middleware/:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const session = useCookie('session')
if (!session.value) {
return navigateTo('/login')
}
})
Then reference it by name:
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>
Global Middleware
Suffix a middleware file with .global to apply it to every route without explicit registration:
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
if (import.meta.client) {
trackPageView(to.fullPath)
}
})
Common Pitfalls
- Missing
<NuxtPage />in parent routes. If you have bothpages/users.vueandpages/users/index.vue, the parent file must render<NuxtPage />. Without it, child routes will never appear. - Forgetting that params are always strings.
route.params.idis'42', not42. Always parse to a number if you need numeric comparison or arithmetic. - Using
<a>instead of<NuxtLink>. Plain anchor tags trigger full page reloads, losing client-side state and defeating the purpose of an SPA. - Catch-all routes swallowing specific routes. A
[...slug].vueat the same level as named pages can cause confusion. Nuxt prioritizes specific routes over catch-all, but test the precedence when your structure is complex. - Middleware running on both server and client. Guard code that touches browser APIs (localStorage, window) must be wrapped in
if (import.meta.client)checks or moved to client-only middleware.
Key Takeaways
- The
pages/directory is the router config. File paths map directly to URL paths with no manual route definitions. - Dynamic routes use
[param]syntax for single segments and[...slug]for catch-all matching. - Nested routes require a parent
.vuefile with<NuxtPage />as the child outlet. NuxtLinkhandles client-side navigation, prefetching, and active-state styling out of the box.- Middleware applied through
definePageMetais the standard mechanism for navigation guards, with support for inline, named, and global variants. - The file-system-as-router convention reduces configuration overhead and makes URL structure visible at a glance.