Nuxt Guide

Pages & Routing

File-based routing, definePageMeta, and data fetching conventions.

File Conventions

Pages live in app/pages/. Nuxt file-based routing applies:

FileRoute
app/pages/index.vue/
app/pages/login.vue/login
app/pages/items/index.vue/items
app/pages/items/[id].vue/items/:id
app/pages/[slug]/index.vue/:slug (dynamic segment)

Page Structure

Pages follow the same <script setup> convention as components. The order inside <script setup> is:

  1. definePageMeta — always first
  2. Composables
  3. Reactive state
  4. Data fetching (called from onMounted)
  5. Handler functions
<script lang="ts" setup>
// 1. Page meta — always first in <script setup>
definePageMeta({
  layout: 'default',
  middleware: ['auth'],
})

// 2. Composables
const { getAll, items, page, pages } = useResource()
const { handleApiAction, loading }   = useFormHandler()

// 3. Reactive state
const dialogs  = reactive({ view: false, edit: false, delete: false })
const selected = ref<IResource | null>(null)

// 4. Data fetching
onMounted(async () => {
  await fetchItems()
})

async function fetchItems() {
  await handleApiAction(
    async () => {
      const res   = await getAll({ page: page.value })
      items.value = res.items
      pages.value = res.pages
    },
    { skipAlert: true },
  )
}
</script>

<template>
  <div class="space-y-6">
    <h1 class="text-2xl font-bold">Items</h1>

    <ResourceTable
      :items="items"
      @view="(item) => { selected = item; dialogs.view = true }"
      @delete="(item) => { selected = item; dialogs.delete = true }"
    />
  </div>
</template>

definePageMeta

Always declared at the top of <script setup>:

definePageMeta({
  layout: 'default',        // matches app/layouts/default.vue
  middleware: ['auth'],     // named middleware file(s) to run
})

Common middleware values:

ValueFileEffect
'auth'app/middleware/auth.tsRedirects to /login if no token
'admin'app/middleware/admin.tsThrows 403 if not admin role
'org'app/middleware/01.org.tsLoads org into state

Dynamic Routes

Dynamic segments use [param] in the file name, accessed with useRoute():

// app/pages/items/[id].vue
const route  = useRoute()
const itemId = route.params.id as string

Data Fetching Pattern

All data fetching is done manually inside onMounted — not with useAsyncData / useFetch at the top level — because SSR is disabled and explicit control is preferred:

onMounted(async () => {
  await loadData()
})

async function loadData() {
  await handleApiAction(
    async () => {
      const res   = await resource.getAll({ page: page.value })
      items.value = res.items
      pages.value = res.pages
    },
    { skipAlert: true },   // suppress toast on initial load
  )
}

Pair with a watch when a filter or page number changes:

watch(page, () => loadData())
watch(searchQuery, useDebounceFn(() => loadData(), 400))