Production-ready Nuxt 4 framework development with SSR, composables, data fetching, server routes, and Cloudflare deployment. Use when: building Nuxt 4 applications, implementing SSR patterns, creating composables, server routes, middleware, data fetching, state management, debugging hydration issues, deploying to Cloudflare, optimizing performance, or setting up testing with Vitest. Keywords: Nuxt 4, Nuxt v4, SSR, universal rendering, Nitro, Vue 3, useState, useFetch, useAsyncData, $fetch, composables, auto-imports, middleware, server routes, API routes, hydration, file-based routing, app directory, SEO, meta tags, useHead, useSeoMeta, transitions, error handling, runtime config, Cloudflare Pages, Cloudflare Workers, NuxtHub, Workers Assets, D1, KV, R2, Durable Objects, Vitest, testing, nuxt performance, lazy loading, code splitting, prerendering, nuxt layers, nuxt modules, nuxt plugins, Vite, hydration mismatch, shallow reactivity, reactive keys, singleton pattern, defineNuxtConfig, defineEventHandler, navigateTo, definePageMeta, useRuntimeConfig, app.vue
/plugin marketplace add secondsky/claude-skills/plugin install nuxt-v4@claude-skillsThis skill is limited to using the following tools:
references/composables.mdreferences/data-fetching.mdreferences/deployment-cloudflare.mdreferences/hydration.mdreferences/performance.mdreferences/server.mdreferences/testing-vitest.mdscripts/init-nuxt-v4.shtemplates/app.vuetemplates/composables/useAuth.tstemplates/nuxt.config.tstemplates/server/api/blog/index.get.tstemplates/tests/components/BlogPost.test.tstemplates/wrangler.tomlProduction-ready patterns for building modern Nuxt 4 applications with SSR, composables, server routes, and Cloudflare deployment.
| Package | Minimum | Recommended |
|---|---|---|
| nuxt | 4.0.0 | 4.2.x |
| vue | 3.5.0 | 3.5.x |
| nitro | 2.10.0 | 2.10.x |
| vite | 6.0.0 | 6.0.x |
| typescript | 5.0.0 | 5.x |
# Create new project
bunx nuxi@latest init my-app
# Development
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Type checking
npm run postinstall # Generates .nuxt directory
bunx nuxi typecheck
# Testing (Vitest)
npm run test
npm run test:watch
# Deploy to Cloudflare
bunx wrangler deploy
my-nuxt-app/
├── app/ # ← New default srcDir in v4
│ ├── assets/ # Build-processed assets (CSS, images)
│ ├── components/ # Auto-imported Vue components
│ ├── composables/ # Auto-imported composables
│ ├── layouts/ # Layout components
│ ├── middleware/ # Route middleware
│ ├── pages/ # File-based routing
│ ├── plugins/ # Nuxt plugins
│ ├── utils/ # Auto-imported utility functions
│ ├── app.vue # Main app component
│ ├── app.config.ts # App-level runtime config
│ ├── error.vue # Error page component
│ └── router.options.ts # Router configuration
│
├── server/ # Server-side code (Nitro)
│ ├── api/ # API endpoints
│ ├── middleware/ # Server middleware
│ ├── plugins/ # Nitro plugins
│ ├── routes/ # Server routes
│ └── utils/ # Server utilities
│
├── public/ # Static assets (served from root)
├── shared/ # Shared code (app + server)
├── content/ # Nuxt Content files (if using)
├── layers/ # Nuxt layers
├── modules/ # Local modules
├── .nuxt/ # Generated files (git ignored)
├── .output/ # Build output (git ignored)
├── nuxt.config.ts # Nuxt configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies
Key Change in v4: The app/ directory is now the default srcDir. All app code goes in app/, server code stays in server/.
This skill includes detailed reference files for deep-dive topics. Load these when you need comprehensive guidance beyond the quick-start examples below.
Load references/composables.md when:
useState, useFetch, or useAsyncDataLoad references/data-fetching.md when:
Load references/server.md when:
Load references/hydration.md when:
ClientOnly components correctlyLoad references/performance.md when:
NuxtImg or NuxtPictureLoad references/testing-vitest.md when:
@nuxt/test-utilsuseFetch, useRoute, etc.)Load references/deployment-cloudflare.md when:
1. Abort Control for Data Fetching
const controller = ref<AbortController>()
const { data } = await useAsyncData(
'users',
() => $fetch('/api/users', { signal: controller.value?.signal })
)
// Abort the request
const abortRequest = () => {
controller.value?.abort()
controller.value = new AbortController()
}
2. Enhanced Error Handling
3. Async Data Handler Extraction
4. TypeScript Plugin Support
@dxup/nuxt module for TS plugins1. Enhanced Chunk Stability
2. Lazy Hydration Without Auto-Imports
<script setup>
const LazyComponent = defineLazyHydrationComponent(() =>
import('./HeavyComponent.vue')
)
</script>
3. Module Lifecycle Hooks
// In a Nuxt module
export default defineNuxtModule({
setup(options, nuxt) {
nuxt.hooks.hook('modules:onInstall', () => {
console.log('Module just installed')
})
nuxt.hooks.hook('modules:onUpgrade', () => {
console.log('Module upgraded')
})
}
})
app/ instead of rootuseFetch/useAsyncData use shallow refs by defaultnull to undefinedexport default defineNuxtConfig({
// Enable future features
future: {
compatibilityVersion: 4
},
// Development config
devtools: { enabled: true },
// Modules
modules: [
'@nuxt/ui',
'@nuxt/content',
'@nuxtjs/tailwindcss'
],
// Runtime config (environment variables)
runtimeConfig: {
// Server-only
apiSecret: process.env.API_SECRET,
databaseUrl: process.env.DATABASE_URL,
// Public (client + server)
public: {
apiBase: process.env.API_BASE || 'https://api.example.com',
appName: 'My App'
}
},
// App config
app: {
head: {
title: 'My Nuxt App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
},
// Nitro config (server)
nitro: {
preset: 'cloudflare-pages', // or 'cloudflare-module'
experimental: {
websocket: true // Enable WebSocket support
}
},
// TypeScript
typescript: {
strict: true,
typeCheck: true
},
// Vite config
vite: {
optimizeDeps: {
include: ['some-heavy-library']
}
}
})
// ✅ Use runtime config for environment variables
const config = useRuntimeConfig()
const apiUrl = config.public.apiBase
// ❌ Don't access process.env directly
const apiUrl = process.env.API_BASE // Won't work in production
Why? Runtime config is reactive and works in both server and client environments. It's also type-safe.
Composables are auto-imported functions that encapsulate reusable logic. Key rule: Always use use prefix (useAuth, useCart).
// ✅ CORRECT: Shared state (survives component unmount)
export const useCounter = () => {
const count = useState('counter', () => 0) // Singleton
return { count }
}
// ❌ WRONG: Creates new instance every time!
export const useCounter = () => {
const count = ref(0) // Not shared
return { count }
}
Rule: useState for shared state. ref for local component state. useState creates a singleton, ref doesn't.
For complete composable patterns including authentication examples, SSR-safe patterns, and advanced state management, load references/composables.md.
| Method | Use Case | SSR | Caching | Reactive |
|---|---|---|---|---|
useFetch | Simple API calls | ✅ | ✅ | ✅ |
useAsyncData | Custom async logic | ✅ | ✅ | ✅ |
$fetch | Client-side only | ❌ | ❌ | ❌ |
Quick Examples:
// useFetch - basic
const { data, error, pending } = await useFetch('/api/users')
// useFetch - reactive params (auto-refetch when page changes)
const page = ref(1)
const { data } = await useFetch('/api/users', { query: { page } })
// useAsyncData - multiple calls
const { data } = await useAsyncData('dashboard', async () => {
const [users, posts] = await Promise.all([
$fetch('/api/users'),
$fetch('/api/posts')
])
return { users, posts }
})
Critical v4 Change: Shallow reactivity is default. Use deep: true option if you need to mutate nested properties.
For comprehensive data fetching patterns including reactive keys, error handling, transform functions, and shallow vs deep reactivity, load references/data-fetching.md.
Nitro provides file-based server routes with HTTP method suffixes:
server/api/users/index.get.ts → GET /api/users
server/api/users/[id].get.ts → GET /api/users/:id
server/api/users/[id].delete.ts → DELETE /api/users/:id
Basic Event Handler:
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id') // URL params
const query = getQuery(event) // Query params
const body = await readBody(event) // Request body
// Error handling
if (!id) {
throw createError({ statusCode: 404, message: 'Not found' })
}
return { id, query, body }
})
For complete server patterns including request/response utilities, cookie handling, database integration (D1 + Drizzle), WebSockets, and middleware, load references/server.md.
Nuxt uses file-based routing in the pages/ directory.
app/pages/
├── index.vue → /
├── about.vue → /about
├── users/
│ ├── index.vue → /users
│ └── [id].vue → /users/:id
└── blog/
├── index.vue → /blog
├── [slug].vue → /blog/:slug
└── [...slug].vue → /blog/* (catch-all)
<!-- app/pages/users/[id].vue -->
<script setup lang="ts">
// Get route params
const route = useRoute()
const userId = route.params.id
// Or use computed for reactivity
const userId = computed(() => route.params.id)
// Fetch user data
const { data: user } = await useFetch(`/api/users/${userId.value}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
</div>
</template>
<script setup>
const goToUser = (id: string) => {
navigateTo(`/users/${id}`)
}
const goBack = () => {
navigateTo(-1) // Go back in history
}
</script>
<template>
<!-- Declarative navigation -->
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink :to="`/users/${user.id}`">View User</NuxtLink>
<!-- Programmatic navigation -->
<button @click="goToUser('123')">View User</button>
</template>
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/login')
}
})
// app/pages/dashboard.vue
<script setup lang="ts">
definePageMeta({
middleware: 'auth'
})
</script>
// app/middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Runs on every route change
console.log('Navigating from', from.path, 'to', to.path)
// Track page view
if (import.meta.client) {
window.gtag('event', 'page_view', {
page_path: to.path
})
}
})
<script setup lang="ts">
useHead({
title: 'My Page Title',
meta: [
{ name: 'description', content: 'Page description' },
{ property: 'og:title', content: 'My Page Title' },
{ property: 'og:description', content: 'Page description' },
{ property: 'og:image', content: 'https://example.com/og-image.jpg' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/my-page' }
]
})
</script>
Better for SEO tags with type safety:
<script setup lang="ts">
useSeoMeta({
title: 'My Page Title',
description: 'Page description',
ogTitle: 'My Page Title',
ogDescription: 'Page description',
ogImage: 'https://example.com/og-image.jpg',
twitterCard: 'summary_large_image'
})
</script>
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
useSeoMeta({
title: post.value?.title,
description: post.value?.excerpt,
ogTitle: post.value?.title,
ogDescription: post.value?.excerpt,
ogImage: post.value?.image,
ogUrl: `https://example.com/blog/${post.value?.slug}`,
twitterCard: 'summary_large_image'
})
</script>
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
titleTemplate: '%s | My App' // "Page Title | My App"
}
}
})
For simple shared state:
// composables/useCart.ts
export const useCart = () => {
const items = useState('cart-items', () => [])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const addItem = (product) => {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
const removeItem = (id) => {
items.value = items.value.filter(i => i.id !== id)
}
return { items, total, addItem, removeItem }
}
bun add pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
// stores/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
isAuthenticated: false
}),
getters: {
userName: (state) => state.user?.name ?? 'Guest'
},
actions: {
async login(email: string, password: string) {
const { data } = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
this.user = data.user
this.isAuthenticated = true
},
logout() {
this.user = null
this.isAuthenticated = false
}
}
})
<!-- app/error.vue -->
<script setup lang="ts">
const props = defineProps({
error: Object
})
const handleError = () => {
clearError({ redirect: '/' })
}
</script>
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>
<button @click="handleError">Go Home</button>
</div>
</template>
<script setup lang="ts">
const error = ref(null)
const handleError = (err) => {
console.error('Component error:', err)
error.value = err
}
</script>
<template>
<NuxtErrorBoundary @error="handleError">
<template #error="{ error, clearError }">
<div>
<h2>Something went wrong</h2>
<p>{{ error }}</p>
<button @click="clearError">Try again</button>
</div>
</template>
<!-- Your component content -->
<MyComponent />
</NuxtErrorBoundary>
</template>
const { data, error, status } = await useFetch('/api/users')
if (error.value) {
showError({
statusCode: error.value.statusCode,
message: error.value.message,
fatal: true // Stops rendering
})
}
Top Causes of "Hydration Mismatch" Errors:
Math.random(), Date.now() → Use useState insteadwindow, localStorage, document → Guard with onMounted() or import.meta.client<ClientOnly> componentQuick Fix:
<!-- ❌ Wrong -->
<script setup>
const id = Math.random()
</script>
<!-- ✅ Right -->
<script setup>
const id = useState('id', () => Math.random())
</script>
For comprehensive hydration debugging including all causes, ClientOnly patterns, and fix strategies, load references/hydration.md.
Key Strategies:
defineAsyncComponent(() => import('~/components/Heavy.vue'))<Component lazy-hydrate="visible|interaction|idle" /><NuxtImg> and <NuxtPicture> for automatic optimizationrouteRules in nuxt.config.ts for SWR, ISR, prerenderingQuick Example:
// nuxt.config.ts - Route rules
routeRules: {
'/': { swr: 3600 }, // Cache 1 hour
'/about': { prerender: true }, // Pre-render at build
'/dashboard/**': { ssr: false } // SPA mode
}
For comprehensive optimization including bundle analysis, Core Web Vitals, lazy hydration patterns, and caching strategies, load references/performance.md.
Setup:
bun add -d @nuxt/test-utils vitest @vue/test-utils happy-dom
Key Features:
mountSuspended() for component testing with Nuxt context@nuxt/test-utils/config for Vitest configurationuseFetch, useRoute, etc.)For complete testing patterns including component tests, composable tests, server route tests, and mocking strategies, load references/testing-vitest.md.
Quick Deploy Commands:
# Cloudflare Pages (Recommended)
npm run build
bunx wrangler pages deploy .output/public
# Cloudflare Workers
npm run build
bunx wrangler deploy
Automatic Deployment: Push to GitHub → Connect Cloudflare Pages → Auto-detected and built
NuxtHub: bun add @nuxthub/core for simplified D1, KV, R2, and Cache API integration.
For comprehensive Cloudflare deployment including wrangler.toml configuration, bindings setup (D1, KV, R2), NuxtHub integration patterns, and environment variables, load references/deployment-cloudflare.md.
// ❌ Wrong
export const useAuth = () => {
const user = ref(null) // New instance every time!
return { user }
}
// ✅ Right
export const useAuth = () => {
const user = useState('auth-user', () => null)
return { user }
}
// ❌ Wrong
const width = window.innerWidth
// ✅ Right
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
// ❌ Wrong
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort(() => Math.random() - 0.5)
})
// ✅ Right
const { data } = await useFetch('/api/users', {
transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})
// ❌ Wrong
const { data } = await useFetch('/api/users')
console.log(data.value.length) // Crashes if error!
// ✅ Right
const { data, error } = await useFetch('/api/users')
if (error.value) {
showToast({ type: 'error', message: error.value.message })
return
}
console.log(data.value.length)
// ❌ Wrong
const apiUrl = process.env.API_URL // Won't work in production!
// ✅ Right
const config = useRuntimeConfig()
const apiUrl = config.public.apiBase
Additional Common Mistakes:
users.get.ts, not users.ts)useFetch<T>() callsparallel: true option for heavy operations)Quick Fixes for Common Issues:
Hydration Mismatch: Check for browser APIs without guards (window, localStorage), non-deterministic values (Math.random(), Date.now()), or wrap in <ClientOnly>
Data Not Refreshing: Ensure params are reactive: useFetch('/api/users', { query: { page } }) where page = ref(1)
TypeScript/Build Errors: Clear cache and regenerate: rm -rf .nuxt .output node_modules/.vite && bun install && npm run dev
Note: Server route 404s usually mean missing .get.ts/.post.ts suffix or wrong directory (server/api/ not app/api/)
See the templates/ directory for:
nuxt.config.tsreferences/composables.md - Advanced composable patternsreferences/data-fetching.md - Complete data fetching guidereferences/server.md - Server route patternsreferences/hydration.md - SSR hydration best practicesreferences/performance.md - Performance optimization strategiesreferences/deployment-cloudflare.md - Comprehensive Cloudflare deployment guidereferences/testing-vitest.md - Vitest testing patternsWithout this skill: ~25,000 tokens (reading docs + trial-and-error) With this skill: ~7,000 tokens (targeted guidance) Savings: ~72% (~18,000 tokens)
This skill helps prevent 20+ common errors:
ref instead of useState for shared stateprocess.client checksprocess.env instead of runtimeConfigkey in useAsyncDataVersion: 1.0.0 | Last Updated: 2025-11-28 | License: MIT
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.