File-based Routing

Routes are automatically created from the pages/ directory structure.

Basic Routes

pages/
├── index.vue              → /
├── about.vue              → /about
├── blog/
│   ├── index.vue         → /blog
│   └── [id].vue          → /blog/:id
└── user/
    └── [username]/
        └── profile.vue   → /user/:username/profile

Dynamic Routes

<!-- pages/blog/[id].vue -->
<template>
  <div>
    <h1>Blog Post: {{ route.params.id }}</h1>
  </div>
</template>

<script setup>
const route = useRoute()
// Access params: route.params.id
</script>

Catch-all Routes

<!-- pages/docs/[...slug].vue -->
<template>
  <div>
    <h1>Docs: {{ route.params.slug.join('/') }}</h1>
  </div>
</template>

<script setup>
const route = useRoute()
// /docs/getting-started → ['getting-started']
// /docs/guide/installation → ['guide', 'installation']
</script>

Layouts

<!-- layouts/default.vue -->
<template>
  <div>
    <AppHeader />
    <slot /> <!-- Page content -->
    <AppFooter />
  </div>
</template>

<!-- pages/index.vue -->
<script setup>
definePageMeta({
  layout: 'default' // or 'dashboard', 'auth', etc.
})
</script>

Data Fetching

Nuxt provides powerful composables for fetching data on server and client.

useFetch - Recommended

<script setup>
// Fetches on server and client
const { data, pending, error, refresh } = await useFetch('/api/users')
</script>

<template>
  <div>
    <p v-if="pending">Loading...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <ul v-else>
      <li v-for="user in data" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <button @click="refresh">Refresh</button>
  </div>
</template>

useAsyncData - Advanced

<script setup>
const route = useRoute()

const { data: post } = await useAsyncData(
  'post-' + route.params.id, // Unique key
  () => $fetch(`/api/posts/${route.params.id}`)
)
</script>

useLazyFetch - Client-only

<script setup>
// Non-blocking, fetches on client
const { pending, data } = useLazyFetch('/api/posts')
</script>

<template>
  <div>
    <!-- Component renders immediately -->
    <p v-if="pending">Loading posts...</p>
    <PostList v-else :posts="data" />
  </div>
</template>

Fetch Options

const { data } = await useFetch('/api/users', {
  method: 'POST',
  body: { name: 'John' },
  headers: { 'Authorization': 'Bearer token' },

  // Cache options
  lazy: false,
  server: true,
  immediate: true,

  // Transformations
  transform: (data) => data.users,
  default: () => [],

  // Watch dependencies
  watch: [someRef],

  // Error handling
  onRequest({ request, options }) {},
  onResponse({ response }) {},
  onResponseError({ response }) {}
})
Performance: useFetch automatically deduplicates requests and caches responses!

Composables

Reusable stateful logic with auto-imported composables.

Built-in Composables

Composable Purpose
useRoute() Access current route information
useRouter() Navigate programmatically
useFetch() Fetch data from API
useState() SSR-friendly reactive state
useCookie() Read/write cookies
useHead() Manage meta tags
useRuntimeConfig() Access runtime configuration

Custom Composable

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState('user', () => null)
  const isLoggedIn = computed(() => !!user.value)

  const login = async (credentials) => {
    const data = await $fetch('/api/login', {
      method: 'POST',
      body: credentials
    })
    user.value = data.user
  }

  const logout = () => {
    user.value = null
  }

  return {
    user,
    isLoggedIn,
    login,
    logout
  }
}

// Usage in any component (auto-imported!)
<script setup>
const { user, isLoggedIn, logout } = useAuth()
</script>

useRouter & useRoute

<script setup>
const router = useRouter()
const route = useRoute()

// Navigation
const goToPost = (id) => {
  router.push(`/blog/${id}`)
  // or router.push({ path: '/blog', query: { id } })
}

// Access params/query
console.log(route.params.id)
console.log(route.query.sort)
</script>

State Management

Manage global state with useState or Pinia.

useState - Built-in

<!-- composables/useCounter.ts -->
export const useCounter = () => {
  const count = useState('counter', () => 0)

  const increment = () => count.value++
  const decrement = () => count.value--

  return { count, increment, decrement }
}

<!-- Any component -->
<script setup>
const { count, increment } = useCounter()
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+</button>
  </div>
</template>

Pinia Store

// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const isLoggedIn = computed(() => !!user.value)

  async function login(credentials) {
    const data = await $fetch('/api/login', {
      method: 'POST',
      body: credentials
    })
    user.value = data.user
  }

  function logout() {
    user.value = null
  }

  return { user, isLoggedIn, login, logout }
})

// Usage
<script setup>
const userStore = useUserStore()
</script>
Tip: Use useState for simple state, Pinia for complex state management with devtools.

Route Middleware

Run code before navigating to routes.

Named Middleware

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()

  if (!isLoggedIn.value) {
    return navigateTo('/login')
  }
})

// pages/dashboard.vue
<script setup>
definePageMeta({
  middleware: 'auth'
})
</script>

Global Middleware

// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // Runs on every route change
  console.log('Navigating to:', to.path)
})

Inline Middleware

<script setup>
definePageMeta({
  middleware: [
    'auth',
    function (to, from) {
      // Inline middleware logic
      const isAdmin = useState('isAdmin')
      if (!isAdmin.value) {
        return abortNavigation()
      }
    }
  ]
})
</script>

Server Routes & API

Create full-stack applications with server routes in the server/ directory.

API Route

// server/api/users.ts
export default defineEventHandler(async (event) => {
  const users = await prisma.user.findMany()
  return users
})

// Access at: /api/users

Dynamic API Route

// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  const user = await prisma.user.findUnique({
    where: { id: parseInt(id) }
  })

  if (!user) {
    throw createError({
      statusCode: 404,
      message: 'User not found'
    })
  }

  return user
})

// Access at: /api/users/123

POST Request

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const user = await prisma.user.create({
    data: {
      name: body.name,
      email: body.email
    }
  })

  return user
})

Server Middleware

// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const token = getHeader(event, 'authorization')

  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized'
    })
  }

  // Verify token...
  event.context.user = decodedUser
})

SEO & Meta Tags

Manage meta tags with useHead and useSeoMeta.

Basic Meta Tags

<script setup>
useHead({
  title: 'My Nuxt App',
  meta: [
    { name: 'description', content: 'My amazing Nuxt application' },
    { property: 'og:title', content: 'My Nuxt App' },
    { property: 'og:image', content: '/og-image.jpg' }
  ],
  link: [
    { rel: 'icon', type: 'image/png', href: '/favicon.png' }
  ]
})
</script>

useSeoMeta - Type-safe

<script setup>
useSeoMeta({
  title: 'My Nuxt App',
  description: 'My amazing Nuxt application',
  ogTitle: 'My Nuxt App',
  ogDescription: 'My amazing Nuxt application',
  ogImage: 'https://example.com/og-image.jpg',
  twitterCard: 'summary_large_image',
})
</script>

Dynamic Meta

<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)

useHead({
  title: () => post.value?.title,
  meta: [
    {
      name: 'description',
      content: () => post.value?.excerpt
    }
  ]
})
</script>

Global Meta (nuxt.config.ts)

export default defineNuxtConfig({
  app: {
    head: {
      title: 'My Nuxt App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'My amazing app' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  }
})
SEO Friendly: Meta tags are rendered on the server for perfect SEO!