---
title: "File-based routing in Nuxt 3, from pages/ to params · vuejs.school"
description: "How Nuxt 3 turns the pages/ folder into routes: static pages, dynamic params, catch-all routes, nested pages with NuxtPage, links and param validation."
canonical_url: "https://vuejs.school/community/blog/file-based-routing-in-nuxt-3"
meta:
  "og:description": "How Nuxt 3 turns the pages/ folder into routes: static pages, dynamic params, catch-all routes, nested pages with NuxtPage, links and param validation."
  "og:image": "https://vuejs.school/_og/d/a_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1zIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1z%2Cdescription_~SG93IE51eHQgMyB0dXJucyB0aGUgcGFnZXMvIGZvbGRlciBpbnRvIHJvdXRlczogc3RhdGljIHBhZ2VzLCBkeW5hbWljIHBhcmFtcywgY2F0Y2gtYWxsIHJvdXRlcywgbmVzdGVkIHBhZ2VzIHdpdGggTnV4dFBhZ2UsIGxpbmtzIGFuZCBwYXJhbSB2YWxpZGF0aW9uLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9maWxlLWJhc2VkLXJvdXRpbmctaW4tbnV4dC0zIg%2Cs_n1pUa6uFI1YCJ5wt.png"
  "og:image:alt": "File-based routing in Nuxt 3, from pages/ to params · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "File-based routing in Nuxt 3, from pages/ to params · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1zIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1z%2Cdescription_~SG93IE51eHQgMyB0dXJucyB0aGUgcGFnZXMvIGZvbGRlciBpbnRvIHJvdXRlczogc3RhdGljIHBhZ2VzLCBkeW5hbWljIHBhcmFtcywgY2F0Y2gtYWxsIHJvdXRlcywgbmVzdGVkIHBhZ2VzIHdpdGggTnV4dFBhZ2UsIGxpbmtzIGFuZCBwYXJhbSB2YWxpZGF0aW9uLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9maWxlLWJhc2VkLXJvdXRpbmctaW4tbnV4dC0zIg%2Cs_n1pUa6uFI1YCJ5wt.png"
  "twitter:image:alt": "File-based routing in Nuxt 3, from pages/ to params · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## 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.vue` serves `/`
- `pages/about.vue` serves `/about`
- `pages/blog/index.vue` serves `/blog`
- `pages/blog/archive.vue` serves `/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:

*app.vue*

```
<template>
  <header>
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
  </header>
  <NuxtPage />
</template>
```

A page is an ordinary component:

*pages/about.vue*

```
<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:

*pages/users/\[id].vue*

```
<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:

*pages/docs/\[...slug].vue*

```
<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:

*pages/\[...slug].vue*

```
<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
```

*pages/users.vue*

```
<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:

*components/UserRow.vue*

```
<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()`:

*pages/login.vue*

```
<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](https://nuxt.com/blog/v3-12) you can try that layout early with one setting:

*nuxt.config.ts*

```
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 `useFetch` and `useAsyncData`; 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 it `pages/`, `app.vue`, `error.vue`, `app.config.ts` and the `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `plugins/` and `utils/` folders. `nuxt.config.ts`, `public/` and `server/` 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.

**Check it stuck**3 questions from the File-based routing quiz.

[**Practice now**](https://vuejs.school/learn/nuxt/file-based-routing?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**What's new in Nuxt 4.6: nuxt/server, typed $fetch, cookie sessions and Vapor**](https://vuejs.school/community/blog/whats-new-in-nuxt-4-6) [Tutorial · 6 min read**useFetch or $fetch? Picking the right one in Nuxt**](https://vuejs.school/community/blog/usefetch-or-fetch-picking-the-right-one-in-nuxt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-02-27","description":"How Nuxt 3 turns the pages/ folder into routes: static pages, dynamic params, catch-all routes, nested pages with NuxtPage, links and param validation.","name":"File-based routing in Nuxt 3, from pages/ to params","url":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3","breadcrumb":{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/file-based-routing-in-nuxt-3"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-02-27","description":"How Nuxt 3 turns the pages/ folder into routes: static pages, dynamic params, catch-all routes, nested pages with NuxtPage, links and param validation.","headline":"File-based routing in Nuxt 3, from pages/ to params","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1zIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1z%2Cdescription_~SG93IE51eHQgMyB0dXJucyB0aGUgcGFnZXMvIGZvbGRlciBpbnRvIHJvdXRlczogc3RhdGljIHBhZ2VzLCBkeW5hbWljIHBhcmFtcywgY2F0Y2gtYWxsIHJvdXRlcywgbmVzdGVkIHBhZ2VzIHdpdGggTnV4dFBhZ2UsIGxpbmtzIGFuZCBwYXJhbSB2YWxpZGF0aW9uLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9maWxlLWJhc2VkLXJvdXRpbmctaW4tbnV4dC0zIg%2Cs_n1pUa6uFI1YCJ5wt.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/file-based-routing-in-nuxt-3#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"File-based routing in Nuxt 3, from pages/ to params","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1zIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1z%2Cdescription_~SG93IE51eHQgMyB0dXJucyB0aGUgcGFnZXMvIGZvbGRlciBpbnRvIHJvdXRlczogc3RhdGljIHBhZ2VzLCBkeW5hbWljIHBhcmFtcywgY2F0Y2gtYWxsIHJvdXRlcywgbmVzdGVkIHBhZ2VzIHdpdGggTnV4dFBhZ2UsIGxpbmtzIGFuZCBwYXJhbSB2YWxpZGF0aW9uLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9maWxlLWJhc2VkLXJvdXRpbmctaW4tbnV4dC0zIg%2Cs_n1pUa6uFI1YCJ5wt.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1zIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~RmlsZS1iYXNlZCByb3V0aW5nIGluIE51eHQgMywgZnJvbSBwYWdlcy8gdG8gcGFyYW1z%2Cdescription_~SG93IE51eHQgMyB0dXJucyB0aGUgcGFnZXMvIGZvbGRlciBpbnRvIHJvdXRlczogc3RhdGljIHBhZ2VzLCBkeW5hbWljIHBhcmFtcywgY2F0Y2gtYWxsIHJvdXRlcywgbmVzdGVkIHBhZ2VzIHdpdGggTnV4dFBhZ2UsIGxpbmtzIGFuZCBwYXJhbSB2YWxpZGF0aW9uLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy9maWxlLWJhc2VkLXJvdXRpbmctaW4tbnV4dC0zIg%2Cs_n1pUa6uFI1YCJ5wt.png"}]}
```