skip to content

In Nuxt 4, which app/pages/ files serve /, /recipes and /recipes/lemon-tart, and how does the recipe page read its slug?

level: juniorimportance: must knowfreq 58%

answer

  1. the directory is the route table
  2. Nuxt 4 source directory is app/
  3. index.vue takes the folder path
  4. square brackets make a param
  5. auto-imported useRoute, then params

basics

~10 s

In Nuxt 4, app/pages/index.vue serves /, app/pages/recipes/index.vue serves /recipes and app/pages/recipes/[slug].vue serves /recipes/lemon-tart. That page reads the segment through the auto-imported useRoute(): route.params.slug is 'lemon-tart'.

solid answer

~30 s

Nuxt 4 builds its Vue Router table from the `app/pages/` directory: every file becomes a route, `index.vue` takes its folder's path, and a name in square brackets becomes a dynamic parameter. So `app/pages/index.vue` is `/`, `app/pages/recipes/index.vue` is `/recipes`, and `app/pages/recipes/[slug].vue` becomes the path `/recipes/:slug()`, a route named `recipes-slug`. Inside that page's `<script setup>` I call the auto-imported `useRoute()` and read `route.params.slug`, which is `'lemon-tart'`. A custom `app.vue` must render `<NuxtPage />` for any page to show, and in Nuxt 4 the folder sits under `app/`, not at the project root as it did in Nuxt 3.

code

vue · 13 lines
vue
<!-- app/pages/recipes/[slug].vue -->
<script setup lang="ts">
const route = useRoute()

// /recipes/lemon-tart -> 'lemon-tart'
const slug = computed(() => String(route.params.slug))
</script>

<template>
  <article>
    <h1>Recipe: {{ slug }}</h1>
  </article>
</template>

go deeper

for a junior

Recall the mapping rules: files under app/pages/ become routes, index.vue takes its folder's path, and square brackets create a param you read with useRoute().params.

for a middle

Explain the generated Vue Router paths, :slug(), :slug? and :slug(.), how route names such as recipes-slug are derived, and why a same-named file beside a folder becomes a parent.

for a senior

Show you keep a large pages tree predictable: catch-all pages that set a real 404 status, no accidental parent files, and a clean move of a Nuxt 3 root pages/ folder into app/.

for a principal

Discuss when file-derived routes stop scaling for several teams, for example splitting pages across layers or overriding paths in page meta, and what that costs in discoverability.

## The directory is the route table In **Nuxt 4**, the **`app/pages/`** directory is where routes come from. At build time Nuxt scans every page file (by default `.vue`, `.js`, `.jsx`, `.mjs`, `.ts` and `.tsx`) and turns each file path into a **Vue Router** route record. Each page is loaded through a dynamic import, so the browser downloads only the code for the route it is showing. Nobody writes a `routes` array for these pages: renaming a file renames its URL, and deleting a file removes the route. That is why an interviewer can hand you a URL and expect you to name the file, or hand you a file tree and expect the URL list back. Two Nuxt 4 facts frame the mapping: - **`app/` is the source directory.** Nuxt 4 changed the default `srcDir` to `app/`, so pages, components, composables and middleware sit under `app/`, and the `~` alias points there. Nuxt 3 kept `pages/` at the project root; Nuxt 4 still auto-detects that older layout, so moving is optional, but new projects use `app/pages/`. - **Pages are optional.** Without an `app/pages/` directory Nuxt leaves `vue-router` out of the bundle and renders `app.vue` alone. Once pages exist, a custom `app.vue` must render **`<NuxtPage />`** where the current page belongs; Nuxt's built-in fallback app already does. ## Mapping the recipe site The rules are few: 1. A file becomes a route at its path relative to `app/pages/`, without the extension. 2. **`index.vue`** takes the path of the folder it sits in. 3. A name in **single square brackets** becomes a required dynamic parameter. 4. Folders add URL segments. Applied to a recipe site: | URL | File | Generated Vue Router path | Route name | |---|---|---|---| | `/` | `app/pages/index.vue` | `/` | `index` | | `/recipes` | `app/pages/recipes/index.vue` | `/recipes` | `recipes` | | `/recipes/lemon-tart` | `app/pages/recipes/[slug].vue` | `/recipes/:slug()` | `recipes-slug` | Route names are derived from the file path, with segments joined by hyphens. That is what `navigateTo({ name: 'recipes-slug', params: { slug: 'apple-pie' } })` or a `<NuxtLink>` can target instead of a hand-built string. ## Reading the parameter Inside `app/pages/recipes/[slug].vue`, the page calls **`useRoute()`**, which Nuxt auto-imports, and reads **`route.params.slug`**. For `/recipes/lemon-tart` that is the string `'lemon-tart'`. The parameter's name is exactly what sits between the brackets, so `[id].vue` gives `route.params.id`. In a template, `$route.params.slug` works too. Unless Nuxt's experimental typed pages are switched on (`experimental.typedPages` defaults to `false`), a param is typed as `string | string[]`, the generic Vue Router shape, which is why page code often narrows it with `String()` before using it as a single value. Two neighbours are *not* the route: - `useRouter()` returns the router instance, which navigates and resolves locations but has no `params` of its own. - `definePageMeta` is a compiler macro that attaches metadata to the route; it returns nothing. Loading the recipe for that slug is a data-fetching concern. A slug that fits the pattern but names no recipe still reaches this page, so the page itself must turn a missing recipe into a 404. ## Optional and catch-all segments The bracket forms differ in what they match and what the page receives: | File | Generated path | `/recipes` | `/recipes/lemon-tart` | `/recipes/a/b` | Param value | |---|---|---|---|---|---| | `recipes/[slug].vue` | `/recipes/:slug()` | no | yes | no | string | | `recipes/[[slug]].vue` | `/recipes/:slug?` | yes | yes | no | string, or empty | | `recipes/[...slug].vue` | `/recipes/:slug(.*)*` | yes | yes | yes | array of segments | A root-level **`app/pages/[...slug].vue`** is the usual custom not-found page: static and more specific routes outrank it, so it only receives leftovers, and for `/about/team` its `route.params.slug` is `['about', 'team']`. ## Common mistakes - Using Nuxt 2's `_slug.vue` naming, which Nuxt 3 and 4 replaced with `[slug].vue`. - Creating `app/pages/recipes.vue` for the list next to a `recipes/` folder. A file beside a same-named folder becomes a **parent route**, so it would also render at `/recipes/lemon-tart`. The list belongs in `recipes/index.vue`. - Putting page files elsewhere under `app/` and expecting routes; only `app/pages/` is scanned. - Treating a catch-all param as one string with slashes; it is an array. - Forgetting `<NuxtPage />` in a custom `app.vue`, which leaves every page without an outlet.

  • What changes if the recipe page is saved as app/pages/recipes/[[slug]].vue instead?
    Double brackets make the parameter optional: the generated path is `/recipes/:slug?`, so the same file also serves `/recipes` with no slug value. That merges the list and the detail into one component that has to branch on whether a slug exists. For a separate list page and detail page I keep `recipes/index.vue` and `recipes/[slug].vue` as two files.
  • How would you add a not-found page for URLs no other page file matches, such as /about/team?
    Add `app/pages/[...slug].vue`, a catch-all whose path is `/:slug(.*)*`. Static and more specific routes still win, so it only receives leftovers, and `route.params.slug` is an array like `['about', 'team']`. By itself it answers with status 200, so during the server render I take `useRequestEvent()` and, when it exists, call `setResponseStatus(event, 404)`.

saying these in an interview costs you the question

  • In Nuxt 4 the pages folder sits at the project root by default
  • Dynamic segments are named with an underscore, like _slug.vue
  • recipes.vue beside a recipes/ folder is the right file for the list page
  • Every page must be registered in a routes array in nuxt.config.ts
  • A [...slug].vue page receives the rest of the path as one string