Load Nuxt framework patterns and conventions
Provides Nuxt framework patterns for data fetching, SEO, error handling, and server routes. Use this to quickly reference best practices for building Nuxt applications.
/plugin marketplace add lttr/claude-marketplace/plugin install nuxt@lttr-claude-marketplaceprime/Note: This command references the
nuxt:nuxtskill for progressive disclosure of additional patterns and module-specific documentation.
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")
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>
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}`),
)
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>
const { data } = await useFetch("/api/users", {
server: false, // Only fetch on client
})
const { data, status, refresh } = await useFetch("/api/users")
// Manually refetch
await refresh()
// Refetch on event
watch(searchQuery, () => refresh())
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({
title: "My Page",
description: "Page description",
ogTitle: "My Page",
ogDescription: "Page description",
ogImage: "https://example.com/image.jpg",
twitterCard: "summary_large_image",
})
definePageMeta({
title: "User Profile",
description: "View user profile",
middleware: ["auth"],
})
showError({
statusCode: 404,
message: "Page not found",
})
// With custom error
showError({
statusCode: 403,
message: "Access denied",
fatal: true,
})
clearError({ redirect: "/" })
const { data, status, error } = await useFetch("/api/users")
if (error.value) {
showError({
statusCode: error.value.statusCode,
message: error.value.message,
})
}
<!-- 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>
// 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
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/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
return {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
}
})
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate and save user
return { success: true, user: body }
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
// Fetch user by id
return { id, name: "User" }
})
export default defineEventHandler(async (event) => {
try {
const data = await fetchData()
return data
} catch (error) {
throw createError({
statusCode: 500,
message: "Internal server error",
})
}
})
// 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/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState("user")
if (!user.value) {
return navigateTo("/login")
}
})
// Usage in page
definePageMeta({
middleware: "auth",
})
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Track page view
console.log("Navigating to:", to.path)
})
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/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.vue -->
<template>
<div>
<header>
<nav>Navigation</nav>
</header>
<main>
<slot />
</main>
<footer>Footer</footer>
</div>
</template>
<!-- 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>
setPageLayout("admin")
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs on client
console.log("Client-side analytics initialized")
})
// plugins/database.server.ts
export default defineNuxtPlugin(() => {
// Only runs on server
return {
provide: {
db: createDatabaseConnection(),
},
}
})
// 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")
Don't manually import these - Nuxt auto-imports them:
Vue APIs: ref, reactive, computed, watch, onMounted, defineProps, defineEmits, defineModel
Nuxt Composables: useState, useFetch, useAsyncData, useRoute, useRouter, navigateTo, useCookie, useHead, useSeoMeta, useRuntimeConfig, showError, clearError
Routing:
pages/index.vue → /pages/about.vue → /aboutpages/users/[id].vue → /users/:idServer API:
server/api/users.get.ts → /api/users (GET)server/api/users.post.ts → /api/users (POST)server/routes/healthz.ts → /healthzLayouts & Middleware:
layouts/default.vue - Default layoutmiddleware/auth.ts - Named middlewaremiddleware/analytics.global.ts - Global middlewareDevelopment:
nuxt dev - Start dev servernuxt dev --host - Expose to networkBuilding:
nuxt build - Production buildnuxt generate - Static site generationnuxt preview - Preview production buildAnalysis:
nuxt analyze - Bundle size analysisnuxt typecheck - Type checkingnuxt info - Environment info