
Nuxt Production
- 321 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
nuxt-production is a Claude Code skill that guides Nuxt.js production deployment, configuration, and release hardening for developers who need to ship Vue-based SSR and static sites reliably.
About
nuxt-production is a Claude Code skill from secondsky/claude-skills focused on Nuxt.js production workflows. It helps developers configure builds, SSR and SSG output, environment variables, hosting adapters, and release checks for Vue 3 applications using the Nuxt framework. Developers reach for nuxt-production when moving a Nuxt app from local development to staging or production, tuning nitro presets, caching, and deployment targets. The skill addresses common production pitfalls in routing, hydration, and build optimization without replacing official Nuxt documentation.
- nuxt-production
Nuxt Production by the numbers
- 321 all-time installs (skills.sh)
- +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,252 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill nuxt-productionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 321 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
How do you deploy Nuxt apps to production?
Use nuxt-production for development tasks
Who is it for?
Frontend developers shipping Nuxt 3 applications who need production build, SSR, and hosting guidance.
Skip if: Greenfield Nuxt prototyping, non-Nuxt Vue projects, or backend API design unrelated to Nuxt deployment.
When should I use this skill?
A developer mentions Nuxt production deploy, nitro preset, SSR go-live, or production nuxt.config tuning.
What you get
Production nuxt.config settings, deployment adapter configuration, environment variable plan, and pre-launch checklist.
- production nuxt.config
- deployment configuration
- launch checklist
Files
Nuxt 4 Production Guide
Hydration, performance, testing, deployment, and migration patterns.
What's New in Nuxt 4
v4.2 Features (Latest)
1. Abort Control for Data Fetching
const controller = ref<AbortController>()
const { data } = await useAsyncData(
'users',
() => $fetch('/api/users', { signal: controller.value?.signal })
)
const abortRequest = () => {
controller.value?.abort()
controller.value = new AbortController()
}2. Async Data Handler Extraction
- 39% smaller client bundles
- Data fetching logic extracted to server chunks
- Automatic optimization (no config needed)
3. Enhanced Error Handling
- Dual error display: custom error page + technical overlay
- Better error messages in development
v4.1 Features
1. Enhanced Chunk Stability
- Import maps prevent cascading hash changes
- Better long-term caching
2. Lazy Hydration
<script setup>
const LazyComponent = defineLazyHydrationComponent(() =>
import('./HeavyComponent.vue')
)
</script>Breaking Changes from v3
| Change | v3 | v4 |
|---|---|---|
| Source directory | Root | app/ |
| Data reactivity | Deep | Shallow (default) |
| Default values | null | undefined |
| Route middleware | Client | Server |
| App manifest | Opt-in | Default |
When to Load References
Load `references/hydration.md` when:
- Debugging "Hydration node mismatch" errors
- Implementing ClientOnly components
- Fixing non-deterministic rendering issues
- Understanding SSR vs client rendering
Load `references/performance.md` when:
- Optimizing Core Web Vitals scores
- Implementing lazy loading and code splitting
- Configuring caching strategies
- Reducing bundle size
Load `references/testing-vitest.md` when:
- Writing component tests with @nuxt/test-utils
- Testing composables with Nuxt context
- Mocking Nuxt APIs (useFetch, useRoute)
- Setting up Vitest configuration
Load `references/deployment-cloudflare.md` when:
- Deploying to Cloudflare Pages or Workers
- Configuring wrangler.toml
- Setting up NuxtHub integration
- Working with D1, KV, R2 bindings
Hydration Best Practices
What Causes Hydration Mismatches
| Cause | Example | Fix |
|---|---|---|
| Non-deterministic values | Math.random() | Use useState |
| Browser APIs on server | window.innerWidth | Use onMounted |
| Date/time on server | new Date() | Use useState or ClientOnly |
| Third-party scripts | Analytics | Use ClientOnly |
Fix Patterns
Non-deterministic Values:
<!-- WRONG -->
<script setup>
const id = Math.random()
</script>
<!-- CORRECT -->
<script setup>
const id = useState('random-id', () => Math.random())
</script>Browser APIs:
<!-- WRONG -->
<script setup>
const width = window.innerWidth // Crashes on server!
</script>
<!-- CORRECT -->
<script setup>
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
</script>ClientOnly Component:
<template>
<!-- Wrap client-only content -->
<ClientOnly>
<MyMapComponent />
<template #fallback>
<div class="skeleton">Loading map...</div>
</template>
</ClientOnly>
</template>Conditional Rendering:
<script setup>
const showWidget = ref(false)
onMounted(() => {
// Only show after hydration
showWidget.value = true
})
</script>
<template>
<AnalyticsWidget v-if="showWidget" />
</template>Performance Optimization
Lazy Loading Components
<script setup>
// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
// With loading/error states
const HeavyChart = defineAsyncComponent({
loader: () => import('~/components/HeavyChart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorFallback,
delay: 200,
timeout: 10000
})
</script>
<template>
<Suspense>
<HeavyChart :data="chartData" />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>Lazy Hydration
<script setup>
// Hydrate when visible in viewport
const LazyComponent = defineLazyHydrationComponent(
() => import('./HeavyComponent.vue'),
{ hydrate: 'visible' }
)
// Hydrate on user interaction
const InteractiveComponent = defineLazyHydrationComponent(
() => import('./InteractiveComponent.vue'),
{ hydrate: 'interaction' }
)
// Hydrate when browser is idle
const IdleComponent = defineLazyHydrationComponent(
() => import('./IdleComponent.vue'),
{ hydrate: 'idle' }
)
</script>Route Caching
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Static pages (prerendered at build)
'/': { prerender: true },
'/about': { prerender: true },
// SWR caching (1 hour)
'/blog/**': { swr: 3600 },
// ISR (regenerate every hour)
'/products/**': { isr: 3600 },
// SPA mode (no SSR)
'/dashboard/**': { ssr: false },
// Static with CDN caching
'/static/**': {
headers: { 'Cache-Control': 'public, max-age=31536000' }
}
}
})Image Optimization
<template>
<!-- Automatic optimization with NuxtImg -->
<NuxtImg
src="/images/hero.jpg"
alt="Hero image"
width="800"
height="400"
loading="lazy"
placeholder
format="webp"
/>
<!-- Responsive images -->
<NuxtPicture
src="/images/product.jpg"
alt="Product"
sizes="sm:100vw md:50vw lg:400px"
:modifiers="{ quality: 80 }"
/>
</template>Testing with Vitest
Setup
bun add -d @nuxt/test-utils vitest @vue/test-utils happy-dom// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom'
}
}
}
})Component Testing
// tests/components/UserCard.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import UserCard from '~/components/UserCard.vue'
describe('UserCard', () => {
it('renders user name', async () => {
const wrapper = await mountSuspended(UserCard, {
props: {
user: { id: 1, name: 'John Doe', email: 'john@example.com' }
}
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('emits delete event', async () => {
const wrapper = await mountSuspended(UserCard, {
props: { user: { id: 1, name: 'John' } }
})
await wrapper.find('[data-test="delete-btn"]').trigger('click')
expect(wrapper.emitted('delete')).toHaveLength(1)
expect(wrapper.emitted('delete')[0]).toEqual([1])
})
})Mocking Composables
// tests/components/Dashboard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime'
import Dashboard from '~/pages/dashboard.vue'
// Mock useFetch
mockNuxtImport('useFetch', () => {
return () => ({
data: ref({ users: [{ id: 1, name: 'John' }] }),
pending: ref(false),
error: ref(null)
})
})
describe('Dashboard', () => {
it('displays users from API', async () => {
const wrapper = await mountSuspended(Dashboard)
expect(wrapper.text()).toContain('John')
})
})Testing Server Routes
// tests/api/users.test.ts
import { describe, it, expect } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
describe('API: /api/users', async () => {
await setup({ server: true })
it('returns users list', async () => {
const users = await $fetch('/api/users')
expect(users).toHaveProperty('users')
expect(Array.isArray(users.users)).toBe(true)
})
it('creates a new user', async () => {
const result = await $fetch('/api/users', {
method: 'POST',
body: { name: 'Jane', email: 'jane@example.com' }
})
expect(result.user.name).toBe('Jane')
})
})Deployment
Cloudflare Pages (Recommended)
# Build and deploy
bun run build
bunx wrangler pages deploy .output/public// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages'
}
})Cloudflare Workers
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-module'
}
})# wrangler.toml
name = "my-nuxt-app"
compatibility_date = "2025-01-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxx-xxx-xxx"
[[kv_namespaces]]
binding = "KV"
id = "xxx-xxx-xxx"Vercel
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel'
}
})Netlify
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'netlify'
}
})NuxtHub (Cloudflare All-in-One)
bun add @nuxthub/core// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxthub/core'],
hub: {
database: true, // D1
kv: true, // KV
blob: true, // R2
cache: true // Cache API
}
})// Usage in server routes
export default defineEventHandler(async (event) => {
const db = hubDatabase()
const kv = hubKV()
const blob = hubBlob()
// Use like regular Cloudflare bindings
const users = await db.prepare('SELECT * FROM users').all()
})Environment Variables
# .env (development)
API_SECRET=dev-secret
DATABASE_URL=http://localhost:8787
# Production (Cloudflare)
wrangler secret put API_SECRET
wrangler secret put DATABASE_URL
# Production (Vercel/Netlify)
# Set in dashboard or CLIMigration from Nuxt 3
Step 1: Update package.json
{
"devDependencies": {
"nuxt": "^4.0.0"
}
}Step 2: Enable Compatibility Mode
// nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
}
})Step 3: Move Files to app/
# Create app directory
mkdir app
# Move files
mv components app/
mv composables app/
mv pages app/
mv layouts app/
mv middleware app/
mv plugins app/
mv assets app/
mv app.vue app/
mv error.vue app/Step 4: Fix Shallow Reactivity
// If mutating data.value properties:
const { data } = await useFetch('/api/user', {
deep: true // Enable deep reactivity
})
// Or replace entire value
data.value = { ...data.value, name: 'New Name' }Step 5: Update Default Values
// v3: data.value is null
// v4: data.value is undefined
// Update null checks
if (data.value === null) // v3
if (!data.value) // v4 (works for both)Common Anti-Patterns
Client-Only Code on Server
// WRONG
const width = window.innerWidth
// CORRECT
if (import.meta.client) {
const width = window.innerWidth
}
// Or use onMounted
onMounted(() => {
const width = window.innerWidth
})Non-Deterministic SSR
// WRONG - Different on server vs client
const id = Math.random()
const time = Date.now()
// CORRECT - Use useState for consistency
const id = useState('id', () => Math.random())
const time = useState('time', () => Date.now())Missing Suspense for Async Components
<!-- WRONG -->
<AsyncComponent />
<!-- CORRECT -->
<Suspense>
<AsyncComponent />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>Troubleshooting
Hydration Mismatch:
- Check for
window,document,localStorageusage - Wrap in
ClientOnlyor useonMounted - Look for
Math.random(),Date.now(),crypto.randomUUID()
Build Errors:
rm -rf .nuxt .output node_modules/.vite && bun installDeployment Fails:
- Check
nitro.presetmatches target - Verify environment variables are set
- Check wrangler.toml bindings match code
Tests Failing:
- Ensure
@nuxt/test-utilsis installed - Check vitest.config.ts has
environment: 'nuxt' - Use
mountSuspendedfor async components
Related Skills
- nuxt-core: Project setup, routing, configuration
- nuxt-data: Composables, data fetching, state
- nuxt-server: Server routes, API patterns
- cloudflare-d1: D1 database patterns
---
Version: 4.0.0 | Last Updated: 2025-12-28 | License: MIT
Cloudflare Deployment - Complete Guide
Comprehensive guide to deploying Nuxt 4 applications on Cloudflare Pages and Workers, including NuxtHub integration and all bindings.
Table of Contents
- Cloudflare Pages
- Cloudflare Workers
- NuxtHub Integration
- Bindings
- Environment Variables
- WebSocket Support
- CI/CD Setup
- Domain Configuration
- Troubleshooting
Cloudflare Pages
Automatic Deployment (Recommended)
Via GitHub Integration:
1. Push your Nuxt project to GitHub 2. Go to Cloudflare Dashboard → Pages 3. Create new project → Connect to Git 4. Select your repository 5. Cloudflare auto-detects Nuxt:
- Build command:
npm run build - Output directory:
.output/public
6. Deploy!
No configuration needed - Cloudflare automatically detects and builds Nuxt applications.
Manual Deployment
# Build for Pages
npm run build
# Deploy with wrangler
npx wrangler pages deploy .output/public --project-name my-nuxt-appConfiguration
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages'
}
})Environment Variables
Set in Cloudflare Dashboard → Pages → Settings → Environment Variables
Or use .env for local development:
# .env
DATABASE_URL=your-database-url
API_SECRET=your-secretCloudflare Workers
Requirements: Compatibility date 2024-09-19 or later
Setup
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-module' // or 'cloudflare-pages'
}
})Build & Deploy
# Build
npm run build
# Deploy
npx wrangler deploywrangler.toml
name = "my-nuxt-app"
main = ".output/server/index.mjs"
compatibility_date = "2024-09-19"
compatibility_flags = ["nodejs_compat"]
# Workers Assets (for static files)
[site]
bucket = ".output/public"
# Environment variables
[vars]
PUBLIC_API_URL = "https://api.example.com"
# Bindings (see below)
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"
[[r2_buckets]]
binding = "R2"
bucket_name = "my-bucket"
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
script_name = "counter-worker"
[[queues.producers]]
binding = "QUEUE"
queue = "my-queue"NuxtHub Integration
NuxtHub provides zero-config Cloudflare integrations for Nuxt.
Installation
npm install @nuxthub/coreConfiguration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxthub/core'],
hub: {
database: true, // D1 database
kv: true, // KV storage
blob: true, // R2 blob storage
cache: true, // Cache API
ai: true // Workers AI
}
})Features
- Zero-config bindings - automatic setup
- Local development - works with
npm run dev - Type safety - full TypeScript support
- Dashboard - visual management at hub.nuxt.com
- Deployment - one-command deploys
Usage
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
// Database
const db = hubDatabase()
const users = await db.select().from(tables.users)
// KV
const kv = hubKV()
await kv.set('users-count', users.length)
// Blob
const blob = hubBlob()
const avatar = await blob.get('avatars/user-1.jpg')
return { users, count: users.length, avatar }
})Bindings
D1 Database
Setup:
# Create database
npx wrangler d1 create my-database
# Output: database_id
# Update wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"Usage with Drizzle:
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from '../database/schema'
export const useDB = (event: H3Event) => {
const { cloudflare } = event.context
if (!cloudflare?.env?.DB) {
throw createError({
statusCode: 500,
message: 'Database not configured'
})
}
return drizzle(cloudflare.env.DB, { schema })
}
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const db = useDB(event)
const users = await db.select().from(schema.users)
return users
})Local Development:
# Install nitro-cloudflare-dev
npm install -D nitro-cloudflare-dev// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nitro-cloudflare-dev']
})KV Storage
Setup:
# Create KV namespace
npx wrangler kv:namespace create MY_KV
# Output: id
# Update wrangler.toml
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"Usage:
// server/utils/kv.ts
export const useKV = (event: H3Event) => {
const { cloudflare } = event.context
if (!cloudflare?.env?.KV) {
throw createError({
statusCode: 500,
message: 'KV not configured'
})
}
return cloudflare.env.KV
}
// server/api/cache/[key].get.ts
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key')
const kv = useKV(event)
const value = await kv.get(key)
if (!value) {
throw createError({
statusCode: 404,
message: 'Key not found'
})
}
return { key, value }
})
// server/api/cache/[key].put.ts
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key')
const { value, ttl } = await readBody(event)
const kv = useKV(event)
await kv.put(key, value, {
expirationTtl: ttl || 3600
})
return { success: true }
})R2 Storage
Setup:
# Create R2 bucket
npx wrangler r2 bucket create my-bucket
# Update wrangler.toml
[[r2_buckets]]
binding = "R2"
bucket_name = "my-bucket"Usage:
// server/utils/r2.ts
export const useR2 = (event: H3Event) => {
const { cloudflare } = event.context
if (!cloudflare?.env?.R2) {
throw createError({
statusCode: 500,
message: 'R2 not configured'
})
}
return cloudflare.env.R2
}
// server/api/upload.post.ts
export default defineEventHandler(async (event) => {
const formData = await readMultipartFormData(event)
const file = formData?.find((item) => item.name === 'file')
if (!file) {
throw createError({
statusCode: 400,
message: 'No file provided'
})
}
const r2 = useR2(event)
// Upload to R2
await r2.put(file.filename, file.data, {
httpMetadata: {
contentType: file.type
}
})
return {
success: true,
filename: file.filename,
url: `https://your-bucket.r2.dev/${file.filename}`
}
})
// server/api/files/[key].get.ts
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key')
const r2 = useR2(event)
const object = await r2.get(key)
if (!object) {
throw createError({
statusCode: 404,
message: 'File not found'
})
}
return object
})Durable Objects
Setup:
# wrangler.toml
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
script_name = "counter-worker"Usage:
// server/api/counter.ts
export default defineEventHandler(async (event) => {
const { cloudflare } = event.context
// Get Durable Object stub
const id = cloudflare.env.COUNTER.idFromName('global')
const stub = cloudflare.env.COUNTER.get(id)
// Call Durable Object
const count = await stub.fetch('https://fake-host/increment')
return { count }
})Queues
Setup:
# Create queue
npx wrangler queues create my-queue
# Update wrangler.toml
[[queues.producers]]
binding = "QUEUE"
queue = "my-queue"
[[queues.consumers]]
queue = "my-queue"Usage:
// server/api/queue-job.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { cloudflare } = event.context
// Send message to queue
await cloudflare.env.QUEUE.send({
type: 'process-user',
userId: body.userId
})
return { queued: true }
})
// Consumer (separate worker or route)
export default {
async queue(batch, env) {
for (const message of batch.messages) {
const { type, userId } = message.body
if (type === 'process-user') {
// Process user
await processUser(userId)
}
message.ack()
}
}
}Workers AI
Setup:
# wrangler.toml
[ai]
binding = "AI"Usage:
// server/api/ai/generate.post.ts
export default defineEventHandler(async (event) => {
const { prompt } = await readBody(event)
const { cloudflare } = event.context
const response = await cloudflare.env.AI.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt
}
)
return response
})Environment Variables
Development (.dev.vars)
# .dev.vars (local development)
API_SECRET=your-secret
DATABASE_URL=your-db-urlProduction (Wrangler Secrets)
# Set secrets
npx wrangler secret put API_SECRET
npx wrangler secret put DATABASE_URLUsage in Code
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: process.env.API_SECRET,
public: {
apiUrl: process.env.PUBLIC_API_URL
}
}
})
// In server routes
export default defineEventHandler((event) => {
const config = useRuntimeConfig()
const secret = config.apiSecret
return { secret }
})WebSocket Support
Enable WebSockets
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
experimental: {
websocket: true
}
}
})WebSocket Route
// server/api/ws.ts
export default defineWebSocketHandler({
open(peer) {
console.log('Client connected:', peer.id)
peer.send({ type: 'connected', message: 'Welcome!' })
},
message(peer, message) {
console.log('Received:', message)
// Echo back
peer.send({ type: 'echo', data: message })
// Broadcast to all
peer.publish('chat', message)
},
close(peer) {
console.log('Client disconnected:', peer.id)
}
})Client Usage
<script setup>
const ws = ref<WebSocket | null>(null)
onMounted(() => {
ws.value = new WebSocket('wss://your-app.pages.dev/api/ws')
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Received:', data)
}
})
onUnmounted(() => {
ws.value?.close()
})
const sendMessage = (message: string) => {
ws.value?.send(JSON.stringify({ message }))
}
</script>CI/CD Setup
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run build
- uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-nuxt-app
directory: .output/publicGitLab CI
# .gitlab-ci.yml
deploy:
image: node:20
script:
- npm install
- npm run build
- npx wrangler pages deploy .output/public --project-name my-nuxt-app
only:
- mainDomain Configuration
Custom Domain (Pages)
1. Go to Cloudflare Dashboard → Pages → Your Project 2. Custom Domains → Add Domain 3. Enter your domain (e.g., app.example.com) 4. Add DNS record (automatic if domain is on Cloudflare)
Custom Domain (Workers)
# wrangler.toml
routes = [
{ pattern = "app.example.com", zone_name = "example.com" }
]Then deploy:
npx wrangler deployTroubleshooting
Build Fails
Issue: Build command not found
Solution:
# wrangler.toml
[build]
command = "npm run build"Missing Bindings
Issue: Binding not available in development
Solution: Install nitro-cloudflare-dev
npm install -D nitro-cloudflare-dev// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nitro-cloudflare-dev']
})WebSocket Not Working
Issue: WebSocket connections fail
Solution: 1. Ensure websocket: true in config 2. Use wss:// protocol 3. Check compatibility date >= 2024-09-19
Environment Variables Not Working
Issue: Env vars undefined in production
Solution: 1. Use wrangler secret put for sensitive values 2. Use [vars] in wrangler.toml for public values 3. Access via useRuntimeConfig()
Database Connection Error
Issue: Database not configured
Solution: 1. Verify binding in wrangler.toml 2. Check database ID is correct 3. Ensure nitro-cloudflare-dev installed for local dev
Best Practices
1. Use NuxtHub for zero-config bindings 2. Enable compression in Nitro config 3. Use route rules for caching 4. Set up CI/CD for automatic deployments 5. Use secrets for sensitive data 6. Enable WebSockets for real-time features 7. Monitor performance with Analytics 8. Use custom domains for production 9. Test locally with nitro-cloudflare-dev 10. Keep compatibility date current
Related Skills
- cloudflare-d1: Deep dive into D1 patterns
- cloudflare-kv: Advanced KV usage
- cloudflare-r2: R2 best practices
- cloudflare-workers-ai: AI integration
- cloudflare-durable-objects: Stateful patterns
- cloudflare-queues: Queue patterns
---
Last Updated: 2025-11-09
Hydration - Best Practices
Complete guide to SSR hydration in Nuxt 4, common issues, and solutions.
Table of Contents
- What is Hydration?
- Common Causes of Hydration Mismatches
- Solutions
- Debugging Hydration Mismatches
- Third-Party Libraries
- Best Practices
- Common Pitfalls
- Checklist
What is Hydration?
Hydration is the process of making server-rendered HTML interactive on the client by attaching Vue's reactivity system and event listeners.
Process: 1. Server renders HTML from Vue components 2. HTML is sent to browser 3. Browser displays HTML (instant visual) 4. Client-side JavaScript loads 5. Vue "hydrates" the HTML (makes it interactive)
Common Causes of Hydration Mismatches
1. Browser APIs
<!-- ❌ Wrong: window doesn't exist on server -->
<script setup>
const width = window.innerWidth
</script>
<template>
<div>Width: {{ width }}</div>
</template>
<!-- ✅ Right: Check environment first -->
<script setup>
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
</script>
<template>
<div>Width: {{ width }}</div>
</template>2. Non-Deterministic Values
<!-- ❌ Wrong: Different value on server vs client -->
<script setup>
const id = Math.random()
const timestamp = Date.now()
</script>
<template>
<div :id="id">{{ timestamp }}</div>
</template>
<!-- ✅ Right: Use useState for consistent values -->
<script setup>
const id = useState('unique-id', () => Math.random())
const timestamp = useState('timestamp', () => Date.now())
</script>
<template>
<div :id="id">{{ timestamp }}</div>
</template>3. Third-Party Libraries
<!-- ❌ Wrong: Library uses window -->
<script setup>
import SomeLibrary from 'some-library'
const instance = new SomeLibrary()
</script>
<!-- ✅ Right: Initialize on client only -->
<script setup>
import type SomeLibrary from 'some-library'
const instance = ref<SomeLibrary | null>(null)
onMounted(async () => {
const { default: Lib } = await import('some-library')
instance.value = new Lib()
})
</script>4. Different HTML Structure
<!-- ❌ Wrong: Different structure on server vs client -->
<script setup>
const isMobile = window.innerWidth < 768
</script>
<template>
<div v-if="isMobile">Mobile view</div>
<div v-else>Desktop view</div>
</template>
<!-- ✅ Right: Same structure, different styling -->
<script setup>
const isMobile = ref(false)
onMounted(() => {
isMobile.value = window.innerWidth < 768
})
</script>
<template>
<div :class="{ mobile: isMobile, desktop: !isMobile }">
<div v-show="isMobile">Mobile view</div>
<div v-show="!isMobile">Desktop view</div>
</div>
</template>Solutions
ClientOnly Component
<template>
<div>
<h1>My Page</h1>
<!-- Only renders on client -->
<ClientOnly>
<HeavyInteractiveComponent />
<!-- Fallback shown during SSR -->
<template #fallback>
<div>Loading interactive content...</div>
</template>
</ClientOnly>
</div>
</template>Process Guards
// Check at runtime
if (process.client) {
// Client-only code
window.addEventListener('resize', handleResize)
}
if (process.server) {
// Server-only code
console.log('Running on server')
}
// Check at compile time
if (import.meta.client) {
// Only bundled for client
}
if (import.meta.server) {
// Only bundled for server
}onMounted Hook
<script setup>
const chart = ref(null)
onMounted(async () => {
// Guaranteed to run on client only
const { default: Chart } = await import('chart.js')
chart.value = new Chart(/* ... */)
})
onUnmounted(() => {
// Cleanup
chart.value?.destroy()
})
</script>useState for Consistency
<script setup>
// ✅ Consistent value across server and client
const theme = useState('theme', () => {
if (import.meta.client) {
return localStorage.getItem('theme') || 'light'
}
return 'light' // Server default
})
// Update on mount
onMounted(() => {
const stored = localStorage.getItem('theme')
if (stored) {
theme.value = stored
}
})
</script>Debugging Hydration Mismatches
Enable Warnings
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
vue: {
template: {
compilerOptions: {
hydration: 'debug'
}
}
}
}
})Console Messages
Look for warnings like:
[Vue warn]: Hydration node mismatch:
- Client vnode: div
- Server rendered DOM: spanIdentify the Component
Vue dev tools will show which component has the mismatch.
Common Patterns
<!-- Pattern 1: Date/Time -->
<!-- ❌ Wrong -->
<div>{{ new Date().toISOString() }}</div>
<!-- ✅ Right -->
<script setup>
const currentTime = ref('')
onMounted(() => {
currentTime.value = new Date().toISOString()
})
</script>
<template>
<ClientOnly>
<div>{{ currentTime }}</div>
</ClientOnly>
</template>
<!-- Pattern 2: Random Values -->
<!-- ❌ Wrong -->
<div :key="Math.random()">Content</div>
<!-- ✅ Right -->
<script setup>
const key = useState('random-key', () => Math.random())
</script>
<template>
<div :key="key">Content</div>
</template>
<!-- Pattern 3: Browser Detection -->
<!-- ❌ Wrong -->
<script setup>
const userAgent = navigator.userAgent
</script>
<!-- ✅ Right -->
<script setup>
const userAgent = ref('')
onMounted(() => {
userAgent.value = navigator.userAgent
})
</script>Third-Party Libraries
Chart Libraries
<script setup>
import type { Chart } from 'chart.js'
const chartInstance = ref<Chart | null>(null)
const chartRef = ref<HTMLCanvasElement>()
onMounted(async () => {
if (!chartRef.value) return
const { Chart } = await import('chart.js/auto')
chartInstance.value = new Chart(chartRef.value, {
type: 'bar',
data: { /* ... */ }
})
})
onUnmounted(() => {
chartInstance.value?.destroy()
})
</script>
<template>
<ClientOnly>
<canvas ref="chartRef" />
</ClientOnly>
</template>Map Libraries
<script setup>
const mapInstance = ref(null)
const mapContainer = ref<HTMLDivElement>()
onMounted(async () => {
if (!mapContainer.value) return
const L = await import('leaflet')
mapInstance.value = L.map(mapContainer.value).setView([51.505, -0.09], 13)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(
mapInstance.value
)
})
onUnmounted(() => {
mapInstance.value?.remove()
})
</script>
<template>
<ClientOnly>
<div ref="mapContainer" style="height: 400px" />
</ClientOnly>
</template>Rich Text Editors
<script setup>
const editor = ref(null)
const editorElement = ref<HTMLDivElement>()
onMounted(async () => {
if (!editorElement.value) return
const Quill = (await import('quill')).default
editor.value = new Quill(editorElement.value, {
theme: 'snow'
})
})
</script>
<template>
<ClientOnly>
<div ref="editorElement" />
<template #fallback>
<textarea placeholder="Loading editor..." />
</template>
</ClientOnly>
</template>Best Practices
1. Always use ClientOnly for browser-dependent components 2. Initialize in onMounted for browser APIs 3. Use useState for values that must be consistent 4. Avoid Math.random() and Date.now() in templates 5. Test SSR rendering in production mode 6. Use v-show instead of v-if when structure must match 7. Provide fallbacks with ClientOnly 8. Clean up resources in onUnmounted 9. Use TypeScript for better type safety 10. Enable hydration warnings in development
Common Pitfalls
❌ Using window/document without guards ❌ Non-deterministic values in templates ❌ Different HTML structure server vs client ❌ Missing ClientOnly for third-party libraries ❌ Not cleaning up event listeners ❌ Using localStorage directly in setup ❌ Forgetting fallbacks ❌ Not testing in production mode
Checklist
- [ ] No window/document access in setup
- [ ] All browser APIs in onMounted
- [ ] Third-party libraries use ClientOnly
- [ ] No Math.random() or Date.now() in templates
- [ ] useState for consistent values
- [ ] Fallbacks provided for ClientOnly
- [ ] Event listeners cleaned up
- [ ] Tested in production mode
- [ ] Hydration warnings enabled in dev
---
Last Updated: 2025-11-09
Performance Optimization
Comprehensive guide to optimizing Nuxt 4 applications for maximum performance.
Table of Contents
- Built-in Optimizations
- Component Optimization
- Image Optimization
- Font Optimization
- Code Splitting
- Caching Strategies
- Bundle Analysis
- Prefetching & Preloading
- Database Optimization
- Vite Optimizations
- Nitro Optimizations
- Performance Monitoring
- Best Practices Checklist
- Common Pitfalls
Built-in Optimizations
Nuxt 4 includes many performance optimizations out of the box:
- Automatic code splitting by route
- Tree shaking to remove unused code
- Minification of JavaScript and CSS
- Preloading of critical resources
- Prefetching of linked pages
- Async data handler extraction (39% smaller bundles in v4.2)
- Import maps for better chunk stability (v4.1)
Component Optimization
Lazy Loading Components
<script setup>
// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
const InteractiveMap = defineAsyncComponent(() =>
import('~/components/InteractiveMap.vue')
)
</script>
<template>
<div>
<!-- Only loads when rendered -->
<HeavyChart v-if="showChart" />
<!-- With loading state -->
<Suspense>
<InteractiveMap />
<template #fallback>
<div>Loading map...</div>
</template>
</Suspense>
</div>
</template>Lazy Hydration
<template>
<div>
<!-- Hydrate when visible -->
<HeavyComponent lazy-hydrate="visible" />
<!-- Hydrate on interaction -->
<InteractiveWidget lazy-hydrate="interaction" />
<!-- Hydrate after idle -->
<LowPriorityComponent lazy-hydrate="idle" />
<!-- Hydrate after delay -->
<DelayedComponent lazy-hydrate="delay:5000" />
</div>
</template>Lazy Hydration Without Auto-Imports (v4.1)
<script setup>
const LazyComponent = defineLazyHydrationComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<template>
<LazyComponent />
</template>Lazy Prefix Convention
components/
├── HeavyChart.vue # Auto-imported
├── LazyHeavyChart.vue # Lazy-loaded
├── InteractiveMap.vue # Auto-imported
└── LazyInteractiveMap.vue # Lazy-loaded<template>
<div>
<!-- Auto-loaded -->
<HeavyChart />
<!-- Lazy-loaded (only when rendered) -->
<LazyHeavyChart v-if="showChart" />
</div>
</template>Image Optimization
NuxtImg
<template>
<!-- Automatic optimization -->
<NuxtImg
src="/images/hero.jpg"
width="800"
height="600"
alt="Hero image"
loading="lazy"
format="webp"
quality="80"
/>
<!-- Responsive -->
<NuxtImg
src="/images/hero.jpg"
sizes="sm:100vw md:50vw lg:400px"
alt="Hero image"
/>
<!-- With provider (Cloudflare Images) -->
<NuxtImg
provider="cloudflare"
src="/images/hero.jpg"
width="800"
height="600"
/>
</template>NuxtPicture
<template>
<!-- Multiple formats (WebP, AVIF, fallback) -->
<NuxtPicture
src="/images/hero.jpg"
:img-attrs="{
alt: 'Hero image',
loading: 'lazy'
}"
sizes="sm:100vw md:50vw lg:400px"
/>
</template>Setup
npm install @nuxt/image// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
// Cloudflare Images
cloudflare: {
baseURL: 'https://your-account.cloudflareimages.com'
},
// Or Cloudflare R2
providers: {
cloudflareR2: {
baseURL: 'https://your-bucket.r2.cloudflarestorage.com'
}
}
}
})Font Optimization
@nuxt/fonts Module
npm install @nuxt/fonts// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/fonts'],
fonts: {
families: [
{ name: 'Inter', provider: 'google' },
{ name: 'Roboto Mono', provider: 'google' }
],
// Automatic optimization
defaults: {
fallbacks: {
'sans-serif': ['Arial', 'sans-serif'],
'monospace': ['Courier New', 'monospace']
}
}
}
})Features:
- Automatic font subsetting
- Preloading of critical fonts
- Font display: swap by default
- Local font caching
- Self-hosted option
Code Splitting
Route-Based Splitting
Automatic in Nuxt - each page is a separate chunk.
pages/
├── index.vue → index-[hash].js
├── about.vue → about-[hash].js
└── blog/
├── index.vue → blog-index-[hash].js
└── [slug].vue → blog-slug-[hash].jsComponent-Based Splitting
<script setup>
// Separate chunk for this component
const HeavyComponent = defineAsyncComponent(() =>
import('~/components/HeavyComponent.vue')
)
</script>Manual Chunks
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk
vendor: ['vue', 'vue-router'],
// Heavy libraries
charts: ['chart.js'],
maps: ['leaflet']
}
}
}
}
}
})Caching Strategies
Route Rules
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Static pages (prerender)
'/': { prerender: true },
'/about': { prerender: true },
// ISR (Incremental Static Regeneration)
'/blog/**': {
swr: 3600, // Revalidate every hour
isr: true
},
// API caching
'/api/posts': {
swr: 600, // 10 minutes
cache: {
maxAge: 600
}
},
// SPA mode (no SSR)
'/dashboard/**': { ssr: false },
// Cache control headers
'/api/config': {
headers: {
'Cache-Control': 'public, max-age=3600, s-maxage=3600'
}
}
}
})API Response Caching
// server/api/posts.get.ts
export default defineCachedEventHandler(
async (event) => {
const posts = await db.posts.findMany()
return posts
},
{
maxAge: 60 * 10, // 10 minutes
name: 'posts-list',
getKey: (event) => {
const query = getQuery(event)
return `posts-${query.page || 1}`
}
}
)Data Fetching Cache
// Automatic caching with key
const { data } = await useFetch('/api/posts', {
key: 'posts-list',
// Cached for this session
})
// Force refresh
const { refresh } = await useFetch('/api/posts', {
key: 'posts-list'
})
await refresh() // Bypasses cacheBundle Analysis
Analyze Bundle Size
# Build with analysis
npx nuxi analyze
# Opens bundle analyzer in browserReduce Bundle Size
1. Remove unused dependencies
npm prune2. Use dynamic imports
// Instead of:
import HeavyLibrary from 'heavy-library'
// Use:
const HeavyLibrary = await import('heavy-library')3. Optimize imports
// Instead of:
import { Button, Input, Select } from '@nuxt/ui'
// Already optimized in Nuxt UI v44. Enable tree shaking
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
terserOptions: {
compress: {
drop_console: true // Remove console.logs
}
}
}
}
})Prefetching & Preloading
NuxtLink Prefetching
<template>
<!-- Prefetch on hover (default) -->
<NuxtLink to="/about">About</NuxtLink>
<!-- Prefetch on visibility -->
<NuxtLink to="/about" prefetch="visible">About</NuxtLink>
<!-- No prefetch -->
<NuxtLink to="/about" :prefetch="false">About</NuxtLink>
</template>Manual Prefetching
const { prefetchComponents } = useNuxtApp()
// Prefetch components
await prefetchComponents('HeavyChart')
await prefetchComponents(['ComponentA', 'ComponentB'])Preload Critical Resources
<script setup>
useHead({
link: [
{
rel: 'preload',
as: 'font',
href: '/fonts/Inter-Regular.woff2',
type: 'font/woff2',
crossorigin: 'anonymous'
}
]
})
</script>Database Optimization
Query Optimization
// ❌ N+1 query problem
const users = await db.users.findMany()
for (const user of users) {
user.posts = await db.posts.findMany({
where: { userId: user.id }
})
}
// ✅ Single query with join
const users = await db.users.findMany({
include: {
posts: true
}
})Pagination
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
const [posts, total] = await Promise.all([
db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
}),
db.posts.count()
])
return {
data: posts,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
})Connection Pooling
With Drizzle + D1, connection pooling is automatic.
Vite Optimizations
Pre-Bundling
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
optimizeDeps: {
include: [
'chart.js',
'leaflet',
'marked'
]
}
}
})Chunk Size Warnings
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
chunkSizeWarningLimit: 1000, // KB
rollupOptions: {
output: {
manualChunks: {
// Split large dependencies
}
}
}
}
}
})Nitro Optimizations
Precomputed Dependencies (v4.2)
Automatic in Nuxt v4.2 - reduces cold start time.
Compression
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
compressPublicAssets: true, // Gzip/Brotli
minify: true
}
})Route Rules
export default defineNuxtConfig({
nitro: {
routeRules: {
// Prerender static routes
'/': { prerender: true },
// Edge caching
'/api/**': { cache: { maxAge: 600 } }
}
}
})Performance Monitoring
Web Vitals
<script setup>
import { useWebVitals } from '~/composables/useWebVitals'
const { lcp, fid, cls, ttfb } = useWebVitals()
// Send to analytics
watch([lcp, fid, cls, ttfb], (metrics) => {
console.log('Web Vitals:', metrics)
})
</script>Performance Marks
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('page:finish', () => {
performance.mark('page-rendered')
const measure = performance.measure(
'page-load',
'navigationStart',
'page-rendered'
)
console.log('Page load time:', measure.duration)
})
})Best Practices Checklist
- [ ] Use lazy loading for heavy components
- [ ] Enable lazy hydration where appropriate
- [ ] Optimize images with NuxtImg/NuxtPicture
- [ ] Use @nuxt/fonts for font optimization
- [ ] Implement route-based caching
- [ ] Cache API responses
- [ ] Analyze bundle size regularly
- [ ] Enable prefetching for linked pages
- [ ] Optimize database queries
- [ ] Use connection pooling
- [ ] Enable compression
- [ ] Monitor Web Vitals
- [ ] Prerender static pages
- [ ] Use ISR for dynamic content
Common Pitfalls
❌ Loading all components eagerly ❌ Not optimizing images ❌ Missing font optimization ❌ No API caching ❌ N+1 query problems ❌ Large bundle sizes ❌ No prefetching ❌ Missing compression ❌ Not monitoring performance
---
Last Updated: 2025-11-09
Testing with Vitest
Comprehensive guide to testing Nuxt 4 applications with Vitest and @nuxt/test-utils.
Table of Contents
- Setup
- Component Testing
- Composable Testing
- Server Route Testing
- Mocking
- Coverage
- E2E Testing
- Best Practices
- Test Organization
- Common Patterns
- Debugging Tests
- Common Pitfalls
- Checklist
Setup
Installation
npm install -D @nuxt/test-utils vitest @vue/test-utils happy-domConfiguration
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'nuxt',
environmentOptions: {
nuxt: {
domEnvironment: 'happy-dom' // or 'jsdom'
}
}
}
})Package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}Component Testing
Basic Component Test
// components/Button.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Button from './Button.vue'
describe('Button', () => {
it('renders correctly', async () => {
const wrapper = await mountSuspended(Button, {
props: {
label: 'Click me'
}
})
expect(wrapper.text()).toContain('Click me')
})
it('emits click event', async () => {
const wrapper = await mountSuspended(Button)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')).toHaveLength(1)
})
it('applies variant classes', async () => {
const wrapper = await mountSuspended(Button, {
props: {
variant: 'primary'
}
})
expect(wrapper.find('button').classes()).toContain('btn-primary')
})
it('is disabled when prop is set', async () => {
const wrapper = await mountSuspended(Button, {
props: {
disabled: true
}
})
expect(wrapper.find('button').attributes('disabled')).toBeDefined()
})
})Component with Slots
// components/Card.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import Card from './Card.vue'
describe('Card', () => {
it('renders slot content', async () => {
const wrapper = await mountSuspended(Card, {
slots: {
default: '<p>Slot content</p>',
header: '<h1>Header</h1>',
footer: '<button>Action</button>'
}
})
expect(wrapper.html()).toContain('Slot content')
expect(wrapper.html()).toContain('Header')
expect(wrapper.html()).toContain('Action')
})
})Component with Composables
// components/UserProfile.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import UserProfile from './UserProfile.vue'
// Mock composable
vi.mock('~/composables/useAuth', () => ({
useAuth: vi.fn(() => ({
user: { value: { name: 'John Doe', email: 'john@example.com' } },
isAuthenticated: { value: true },
logout: vi.fn()
}))
}))
describe('UserProfile', () => {
it('displays user information', async () => {
const wrapper = await mountSuspended(UserProfile)
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('calls logout when button clicked', async () => {
const { useAuth } = await import('~/composables/useAuth')
const wrapper = await mountSuspended(UserProfile)
await wrapper.find('button').trigger('click')
expect(useAuth().logout).toHaveBeenCalled()
})
})Composable Testing
Basic Composable Test
// composables/useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('starts at 0', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('increments count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
increment()
expect(count.value).toBe(2)
})
it('decrements count', () => {
const { count, increment, decrement } = useCounter()
increment()
increment()
increment()
expect(count.value).toBe(3)
decrement()
expect(count.value).toBe(2)
})
it('resets to 0', () => {
const { count, increment, reset } = useCounter()
increment()
increment()
expect(count.value).toBe(2)
reset()
expect(count.value).toBe(0)
})
})Async Composable Test
// composables/useApi.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useApi } from './useApi'
// Mock $fetch
global.$fetch = vi.fn()
describe('useApi', () => {
it('fetches data successfully', async () => {
const mockData = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]
global.$fetch.mockResolvedValue(mockData)
const api = useApi('/api/users')
await api.execute()
expect(api.data.value).toEqual(mockData)
expect(api.error.value).toBeNull()
expect(api.isLoading.value).toBe(false)
})
it('handles errors', async () => {
global.$fetch.mockRejectedValue(new Error('Network error'))
const api = useApi('/api/users')
try {
await api.execute()
} catch (err) {
expect(api.error.value?.message).toBe('Network error')
expect(api.data.value).toBeNull()
}
})
it('sets loading state', async () => {
global.$fetch.mockImplementation(() => {
return new Promise((resolve) => {
setTimeout(() => resolve([]), 100)
})
})
const api = useApi('/api/users')
const promise = api.execute()
expect(api.isLoading.value).toBe(true)
await promise
expect(api.isLoading.value).toBe(false)
})
})Server Route Testing
API Route Test
// server/api/users.get.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils/runtime'
describe('/api/users', async () => {
await setup({
server: true
})
it('returns users list', async () => {
const users = await $fetch('/api/users')
expect(users).toBeInstanceOf(Array)
expect(users.length).toBeGreaterThan(0)
expect(users[0]).toHaveProperty('id')
expect(users[0]).toHaveProperty('name')
})
it('filters by query param', async () => {
const users = await $fetch('/api/users', {
query: { role: 'admin' }
})
expect(users.every(u => u.role === 'admin')).toBe(true)
})
it('returns 404 for non-existent user', async () => {
try {
await $fetch('/api/users/non-existent')
} catch (error) {
expect(error.response?.status).toBe(404)
}
})
})POST Request Test
// server/api/users.post.test.ts
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils/runtime'
describe('POST /api/users', async () => {
await setup({
server: true
})
it('creates new user', async () => {
const newUser = {
name: 'Test User',
email: 'test@example.com'
}
const created = await $fetch('/api/users', {
method: 'POST',
body: newUser
})
expect(created).toHaveProperty('id')
expect(created.name).toBe(newUser.name)
expect(created.email).toBe(newUser.email)
})
it('validates required fields', async () => {
try {
await $fetch('/api/users', {
method: 'POST',
body: { name: 'Test' } // Missing email
})
} catch (error) {
expect(error.response?.status).toBe(400)
expect(error.data).toHaveProperty('message')
}
})
})Mocking
Mock useFetch
import { vi } from 'vitest'
vi.mock('#app', () => ({
useFetch: vi.fn((url) => ({
data: ref([
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]),
error: ref(null),
pending: ref(false),
refresh: vi.fn()
}))
}))Mock useState
vi.mock('#app', () => ({
useState: vi.fn((key, init) => {
const state = ref(init())
return state
})
}))Mock useRoute
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
params: { id: '123' },
query: { page: '1' },
path: '/users/123'
}))
}))Mock Composables
vi.mock('~/composables/useAuth', () => ({
useAuth: vi.fn(() => ({
user: ref({ id: '1', name: 'John Doe' }),
isAuthenticated: ref(true),
login: vi.fn(),
logout: vi.fn()
}))
}))Coverage
Setup Coverage
npm install -D @vitest/coverage-v8// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/**',
'.nuxt/**',
'.output/**',
'**/*.test.ts',
'**/*.spec.ts'
]
}
}
})Run Coverage
npm run test:coverageCoverage Thresholds
// vitest.config.ts
export default defineVitestConfig({
test: {
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
})E2E Testing
With Playwright
npm install -D @playwright/test// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:3000'
},
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: true
}
})// tests/e2e/login.test.ts
import { test, expect } from '@playwright/test'
test('user can login', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Dashboard')
})Best Practices
1. Test behavior, not implementation 2. Use meaningful test descriptions 3. Group related tests with describe 4. Keep tests isolated (no shared state) 5. Mock external dependencies 6. Test edge cases and error scenarios 7. Use TypeScript for type safety 8. Maintain high coverage (>80%) 9. Run tests in CI/CD 10. Keep tests fast (<100ms per test)
Test Organization
tests/
├── components/
│ ├── Button.test.ts
│ ├── Card.test.ts
│ └── Modal.test.ts
├── composables/
│ ├── useAuth.test.ts
│ ├── useCart.test.ts
│ └── useCounter.test.ts
├── server/
│ ├── api/
│ │ ├── users.get.test.ts
│ │ └── users.post.test.ts
│ └── utils/
│ └── validation.test.ts
└── e2e/
├── login.test.ts
├── checkout.test.ts
└── navigation.test.tsCommon Patterns
Test Setup
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
describe('MyComponent', () => {
beforeEach(() => {
// Run before each test
})
afterEach(() => {
// Run after each test
})
it('test 1', () => {})
it('test 2', () => {})
})Async Tests
it('fetches data', async () => {
const data = await fetchData()
expect(data).toBeDefined()
})Testing Reactivity
it('updates reactively', async () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
await nextTick() // Wait for reactivity
expect(count.value).toBe(1)
})Snapshot Testing
it('matches snapshot', async () => {
const wrapper = await mountSuspended(Button)
expect(wrapper.html()).toMatchSnapshot()
})Debugging Tests
Run Single Test
npm run test -- Button.test.tsWatch Mode
npm run test:watchUI Mode
npm run test:uiDebug in VS Code
// .vscode/launch.json
{
"type": "node",
"request": "launch",
"name": "Debug Vitest Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "test"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}Common Pitfalls
❌ Testing implementation details ❌ Shared state between tests ❌ Not mocking external dependencies ❌ Missing async/await ❌ Not testing error cases ❌ Slow tests ❌ Low coverage ❌ Not running tests in CI
Checklist
- [ ] All components have tests
- [ ] All composables have tests
- [ ] All API routes have tests
- [ ] Coverage > 80%
- [ ] Tests pass in CI
- [ ] E2E tests for critical flows
- [ ] Mocks are properly set up
- [ ] Tests are fast (<100ms each)
- [ ] Edge cases are covered
- [ ] Error scenarios are tested
---
Last Updated: 2025-12-28
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import BlogPost from '~/components/BlogPost.vue'
describe('BlogPost', () => {
const mockPost = {
id: '1',
title: 'Test Post',
excerpt: 'This is a test post',
content: 'Full content here',
author: 'John Doe',
createdAt: new Date('2025-01-01')
}
it('renders post title', async () => {
const wrapper = await mountSuspended(BlogPost, {
props: { post: mockPost }
})
expect(wrapper.text()).toContain('Test Post')
})
it('renders post excerpt', async () => {
const wrapper = await mountSuspended(BlogPost, {
props: { post: mockPost }
})
expect(wrapper.text()).toContain('This is a test post')
})
it('renders author name', async () => {
const wrapper = await mountSuspended(BlogPost, {
props: { post: mockPost }
})
expect(wrapper.text()).toContain('John Doe')
})
it('emits click event when clicked', async () => {
const wrapper = await mountSuspended(BlogPost, {
props: { post: mockPost }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})
# Cloudflare Workers configuration
name = "my-nuxt-app"
main = ".output/server/index.mjs"
compatibility_date = "2025-12-19"
compatibility_flags = ["nodejs_compat"]
# Workers Assets (static files)
[site]
bucket = ".output/public"
# Environment variables (public, non-sensitive)
[vars]
PUBLIC_API_URL = "https://api.example.com"
APP_NAME = "My Nuxt App"
# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id" # Get from: npx wrangler d1 create my-database
# KV Storage
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id" # Get from: npx wrangler kv:namespace create MY_KV
# R2 Storage
[[r2_buckets]]
binding = "R2"
bucket_name = "my-bucket" # Create first: npx wrangler r2 bucket create my-bucket
# Durable Objects (optional)
# [[durable_objects.bindings]]
# name = "COUNTER"
# class_name = "Counter"
# script_name = "counter-worker"
# Queues (optional)
# [[queues.producers]]
# binding = "QUEUE"
# queue = "my-queue"
# Workers AI (optional)
# [ai]
# binding = "AI"
Related skills
How it compares
Use nuxt-production for Nuxt-specific ship tasks; general Next.js or Vue skills apply to different frameworks.
FAQ
What does nuxt-production help with?
nuxt-production helps developers configure Nuxt.js for production, including builds, SSR and SSG output, environment variables, and deployment adapters. The skill targets go-live readiness for Vue-based Nuxt applications.
Is nuxt-production for new Nuxt projects?
nuxt-production focuses on shipping and production hardening rather than initial scaffolding. Developers building new features locally should use general Nuxt development skills first, then invoke nuxt-production before launch.