Reading a Nuxt app's routes means reading its pages/ folder. Learn the naming rules and you can look at that folder and list the URLs it serves, or take a URL and find the file that renders it.
The examples use Nuxt 3.15 with the Nuxt 3 layout: pages/, components/ and composables/ at the project root, next to nuxt.config.ts. To follow along, start a project with npx nuxi@latest init routing-demo.
A file becomes a route
Nuxt scans pages/ at build time and hands Vue Router a route for each file. The file path is the URL path, with index.vue standing for the folder itself.
pages/index.vueserves/pages/about.vueserves/aboutpages/blog/index.vueserves/blogpages/blog/archive.vueserves/blog/archive
Nuxt watches the folder in development, so a new file is a new route the moment you save it, with no restart. The folder is optional: without pages/, Nuxt leaves Vue Router out of the bundle, which keeps a one-page app smaller.
The starter's app.vue shows a welcome screen. For your pages to appear, replace it with a <NuxtPage />, the place where the current page is rendered (it wraps Vue Router's <RouterView>). Anything around it, like this header, appears on every page:
<template>
<header>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</header>
<NuxtPage />
</template>
A page is an ordinary component:
<script setup lang="ts">
const since = 2025
</script>
<template>
<main>
<h1>About</h1>
<p>Teaching Vue since {{ since }}.</p>
</main>
</template>
Give each page a single root element, as this one does with <main>. Route transitions between pages need it, and an HTML comment at the top of the template counts as a second root. Keeping to one root from the start saves a confusing warning later.
Dynamic segments with square brackets
A file name in square brackets is a parameter: a part of the URL that can be anything. Create pages/users/[id].vue and it serves /users/1, /users/42 and /users/anything. Inside the page, useRoute() gives you the current route, and route.params.id holds the value:
<script setup lang="ts">
const route = useRoute()
const id = route.params.id as string
</script>
<template>
<main>
<h1>User {{ id }}</h1>
</main>
</template>
useRoute is auto-imported in Nuxt, like ref and computed. The as string is there because Vue Router types a param as string | string[]; a single bracket segment is always a string, so the cast is honest. In the template you can also read $route.params.id directly without the script.
Brackets can be part of a name too. pages/users/[id]-settings.vue serves /users/42-settings, and a folder can be dynamic: pages/[category]/[slug].vue gives you two params from /vue/reactivity.
Catch-all routes and a custom 404 page
Three dots inside the brackets match the rest of the URL, however many segments it has. Create pages/docs/[...slug].vue and it serves /docs/intro, /docs/guide/routing and anything else under /docs/. This time the param is an array, one item per segment:
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug
const parts = Array.isArray(slug) ? slug : []
const path = parts.join('/')
</script>
<template>
<main>
<h1>Docs: {{ path }}</h1>
</main>
</template>
A catch-all also matches its own folder: /docs reaches this page with slug set to an empty string, not an array. That is why the code checks Array.isArray before calling join, which a string does not have. On /docs itself, show an index or redirect rather than an empty heading.
A catch-all at the top level, pages/[...slug].vue, matches every URL that no other page claims (/about still goes to about.vue), so it can be your custom 404 page. Nuxt sends it with status 200 like any other page, so set the status yourself:
<script setup lang="ts">
const event = useRequestEvent()
if (event) {
setResponseStatus(event, 404)
}
</script>
<template>
<main>
<h1>Page not found</h1>
<NuxtLink to="/">Back to the home page</NuxtLink>
</main>
</template>
useRequestEvent() returns the incoming request on the server and undefined in the browser, where there is no response status to set.
Nested pages with NuxtPage
Sometimes a group of pages shares a frame: a sidebar on every user page, or tabs on a settings screen. Nuxt expresses that with a file and a folder of the same name. The file is the parent, the folder holds the children, and the parent decides where the child appears with its own <NuxtPage />.
pages/
users.vue # parent, serves nothing on its own
users/
index.vue # /users
[id].vue # /users/42
<template>
<div class="users-layout">
<aside>
<NuxtLink to="/users">All users</NuxtLink>
</aside>
<NuxtPage />
</div>
</template>
When the URL is /users/42, Nuxt renders users.vue and, inside its <NuxtPage />, users/[id].vue. Navigating from one user to another re-renders only the child; the sidebar stays.
Use nested pages when the parent is part of the URL. If you only want a shared frame around pages, with no shared URL prefix, a layout in layouts/ is the better tool.
Link to pages and navigate from code
<NuxtLink> is how pages link to each other. It renders an <a>, so the link works without JavaScript, and when the app is running it navigates on the client without a full reload. Use a plain path, or name the route and let Nuxt build the path:
<script setup lang="ts">
defineProps<{ id: number, name: string }>()
</script>
<template>
<li>
<NuxtLink :to="`/users/${id}`">{{ name }}</NuxtLink>
<NuxtLink :to="{ name: 'users-id', params: { id } }">{{ name }}</NuxtLink>
</li>
</template>
Both links go to the same place. Route names are generated from the file path with hyphens, so pages/users/[id].vue is named users-id. With a name, Vue Router builds the URL and encodes the param, so a value containing a / still works. A name does not survive a move any better than a path: moving the file changes both.
To highlight the current item in a navigation bar, style the classes <NuxtLink> adds: router-link-exact-active on a link to the current page, and router-link-active on that link and on links to its parent pages. Parent means nested: the "All users" link in users.vue stays active on /users/42, but a link to /blog is not active on /blog/archive, because no blog.vue wraps those pages.
To navigate from code, after a form submits or a login succeeds, call navigateTo():
<script setup lang="ts">
const email = ref('')
async function submit() {
await $fetch('/api/login', { method: 'POST', body: { email: email.value } })
await navigateTo('/users')
}
</script>
<template>
<main>
<form @submit.prevent="submit">
<input v-model="email" type="email" />
<button>Sign in</button>
</form>
</main>
</template>
navigateTo is auto-imported, works on the server and the client, and returns a promise, so await it. It is what you use instead of useRouter().push() in Nuxt.
Reject invalid params before the page loads
A dynamic segment accepts anything, including /users/not-a-number. Rather than letting the page render and fail while fetching, tell Nuxt what a valid param looks like. definePageMeta() is a compiler macro for page settings, and its validate option receives the route and returns true or false. Add this to the <script setup> of pages/users/[id].vue:
definePageMeta({
validate: (route) => {
return typeof route.params.id === 'string' && /^\d+$/.test(route.params.id)
},
})
When validate returns false, Nuxt treats the URL as not matching this page. If no other page matches either, the visitor gets a 404, rendered by your error.vue if you have one. The check runs before the page is created, on the server for the first request and on the client for later navigations, so a bad URL never reaches your data fetching.
Try the app/ folder layout early
The Nuxt team has said that the next major version moves application code into an app/ folder, so pages/ becomes app/pages/. Since Nuxt 3.12 you can try that layout early with one setting:
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
})
Before you add it:
- It turns on the behaviour changes planned for the next major version, not only the new folder. Several affect
useFetchanduseAsyncData; the 3.12 post links the full list. - It moves nothing. Without an
app/folder, Nuxt keeps reading the root layout. - To switch, create
app/and move into itpages/,app.vue,error.vue,app.config.tsand theassets/,components/,composables/,layouts/,middleware/,plugins/andutils/folders.nuxt.config.ts,public/andserver/stay at the root.
The naming rules in this post work the same in both layouts. For a new project the default Nuxt 3 layout is the safe choice.