Files
gh-lttr-claude-marketplace-…/skills/nuxt/references/nuxt-patterns.md
2025-11-30 08:38:06 +08:00

8.4 KiB

Nuxt Common Patterns

Data Fetching

useFetch for API Calls

Use useFetch for API endpoints. It runs on both server and client, with automatic hydration.

const { data, status, error, refresh } = await useFetch("/api/users")

// With query parameters
const { data, status } = await useFetch("/api/users", {
  query: { limit: 10, page: 1 },
})

// With type safety
interface User {
  id: number
  name: string
}
const { data, status, error } = await useFetch<User[]>("/api/users")

Always handle all states in templates:

<template>
  <div v-if="status === 'pending'">Loading...</div>
  <div v-else-if="status === 'error'">
    <p>Error: {{ error?.message }}</p>
  </div>
  <div v-else-if="data">
    <!-- Success state -->
    <ul>
      <li v-for="user of data" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

useAsyncData for Complex Data

Use useAsyncData when you need more control or complex transformations.

const { data, status, error } = await useAsyncData("users", async () => {
  const users = await $fetch("/api/users")
  const stats = await $fetch("/api/stats")
  return { users, stats }
})

// With caching key
const { data, status, error } = await useAsyncData(`user-${id}`, () =>
  $fetch(`/api/users/${id}`),
)

Lazy Fetching

Use lazy variants when you don't want to block navigation:

// Non-blocking
const { status, data } = await useLazyFetch('/api/users')

// Show loading state
<div v-if="status === 'pending'">Loading...</div>
<div v-else>{{ data }}</div>

Client-Only Fetching

const { data } = await useFetch("/api/users", {
  server: false, // Only fetch on client
})

Refresh and Refetch

const { data, status, refresh } = await useFetch("/api/users")

// Manually refetch
await refresh()

// Refetch on event
watch(searchQuery, () => refresh())

SEO and Meta Tags

useHead

useHead({
  title: "My Page",
  meta: [
    { name: "description", content: "Page description" },
    { property: "og:title", content: "My Page" },
  ],
  link: [{ rel: "canonical", href: "https://example.com/page" }],
})

useSeoMeta (Type-Safe)

useSeoMeta({
  title: "My Page",
  description: "Page description",
  ogTitle: "My Page",
  ogDescription: "Page description",
  ogImage: "https://example.com/image.jpg",
  twitterCard: "summary_large_image",
})

definePageMeta

definePageMeta({
  title: "User Profile",
  description: "View user profile",
  middleware: ["auth"],
})

Error Handling

Show Error Page

showError({
  statusCode: 404,
  message: "Page not found",
})

// With custom error
showError({
  statusCode: 403,
  message: "Access denied",
  fatal: true,
})

Clear Error

clearError({ redirect: "/" })

Handle Errors in Data Fetching

const { data, status, error } = await useFetch("/api/users")

if (error.value) {
  showError({
    statusCode: error.value.statusCode,
    message: error.value.message,
  })
}

Error Component

<!-- error.vue -->
<template>
  <div>
    <h1>{{ error.statusCode }}</h1>
    <p>{{ error.message }}</p>
    <button @click="handleError">Go Home</button>
  </div>
</template>

<script setup lang="ts">
const { error } = defineProps<{
  error: { statusCode: number; message: string }
}>()

function handleError() {
  clearError({ redirect: "/" })
}
</script>

Environment Variables and Config

Runtime Config

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Private (server-only)
    apiSecret: process.env.API_SECRET,
    databaseUrl: process.env.DATABASE_URL,

    // Public (exposed to client)
    public: {
      apiBase: process.env.API_BASE_URL || "http://localhost:3000",
      environment: process.env.NODE_ENV,
    },
  },
})

// Usage
const config = useRuntimeConfig()
console.log(config.public.apiBase) // Available everywhere
console.log(config.apiSecret) // Server-only

App Config

For non-sensitive configuration that can be updated at runtime:

// app.config.ts
export default defineAppConfig({
  theme: {
    primaryColor: "#3b82f6",
  },
})

// Usage
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)

Server API Routes

GET Request

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)

  return {
    users: [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
    ],
  }
})

POST Request

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

  // Validate and save user
  return { success: true, user: body }
})

Dynamic Routes

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

  // Fetch user by id
  return { id, name: "User" }
})

Error Handling in API Routes

export default defineEventHandler(async (event) => {
  try {
    const data = await fetchData()
    return data
  } catch (error) {
    throw createError({
      statusCode: 500,
      message: "Internal server error",
    })
  }
})

Protected API Routes

// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
  const session = await requireUserSession(event)

  if (!session.user.isAdmin) {
    throw createError({
      statusCode: 403,
      message: "Forbidden",
    })
  }

  return { users: [] }
})

Middleware

Route Middleware

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const user = useState("user")

  if (!user.value) {
    return navigateTo("/login")
  }
})

// Usage in page
definePageMeta({
  middleware: "auth",
})

Global Middleware

// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // Track page view
  console.log("Navigating to:", to.path)
})

State Management

useState

For shared state across components:

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>("user", () => null)
  const isAuthenticated = computed(() => !!user.value)

  async function login(credentials: LoginCredentials) {
    const response = await $fetch("/api/auth/login", {
      method: "POST",
      body: credentials,
    })
    user.value = response.user
  }

  function logout() {
    user.value = null
  }

  return {
    user,
    isAuthenticated,
    login,
    logout,
  }
}

// Usage in component
const { user, login, logout } = useAuth()

Composables

Auto-Import from composables/

// composables/useCounter.ts
export const useCounter = () => {
  const count = ref(0)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  return {
    count,
    increment,
    decrement,
  }
}

// Usage (auto-imported)
const { count, increment } = useCounter()

Layouts

Default Layout

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <nav>Navigation</nav>
    </header>
    <main>
      <slot />
    </main>
    <footer>Footer</footer>
  </div>
</template>

Custom Layout

<!-- layouts/admin.vue -->
<template>
  <div class="admin-layout">
    <aside>Sidebar</aside>
    <main>
      <slot />
    </main>
  </div>
</template>

<!-- Usage in page -->
<script setup lang="ts">
definePageMeta({
  layout: "admin",
})
</script>

Dynamic Layout

setPageLayout("admin")

Plugins

Client-Only Plugin

// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
  // Only runs on client
  console.log("Client-side analytics initialized")
})

Server-Only Plugin

// plugins/database.server.ts
export default defineNuxtPlugin(() => {
  // Only runs on server
  return {
    provide: {
      db: createDatabaseConnection(),
    },
  }
})

Universal Plugin

// plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: "/api",
    onResponseError({ response }) {
      if (response.status === 401) {
        navigateTo("/login")
      }
    },
  })

  return {
    provide: {
      api,
    },
  }
})

// Usage
const { $api } = useNuxtApp()
const data = await $api("/users")