From martinholovsky-claude-skills-generator
Guides TDD-first Vue 3/Nuxt 3 development with Composition API, TypeScript, Pinia, Vitest tests, composables, SSR/SSG, performance optimization, and client-side security.
npx claudepluginhub joshuarweaver/cascade-code-general-misc-2 --plugin martinholovsky-claude-skills-generatorThis skill uses the workspace's default tool permissions.
**Risk Level**: MEDIUM
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Risk Level: MEDIUM
Expertise Areas:
Target Users: Frontend engineers building modern, performant, type-safe web applications
Key Focus: Type-safe component architecture, composable logic, SSR/SSG patterns, and client-side security
// tests/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import UserCard from '~/components/UserCard.vue'
describe('UserCard', () => {
it('displays user name and email', () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: '1',
name: 'John Doe',
email: 'john@example.com'
}
},
global: {
plugins: [createTestingPinia()]
}
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('emits select event when clicked', async () => {
const wrapper = mount(UserCard, {
props: {
user: { id: '1', name: 'John', email: 'john@test.com' }
}
})
await wrapper.trigger('click')
expect(wrapper.emitted('select')).toBeTruthy()
expect(wrapper.emitted('select')[0]).toEqual(['1'])
})
it('shows loading state', () => {
const wrapper = mount(UserCard, {
props: {
user: null,
loading: true
}
})
expect(wrapper.find('[data-testid="loading-skeleton"]').exists()).toBe(true)
})
})
// tests/composables/useAsyncData.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useAsyncData } from '~/composables/useAsyncData'
describe('useAsyncData', () => {
it('fetches data successfully', async () => {
const mockData = { id: 1, name: 'Test' }
const fetcher = vi.fn().mockResolvedValue(mockData)
const { data, loading, error, execute } = useAsyncData(fetcher, {
immediate: false
})
expect(data.value).toBeNull()
expect(loading.value).toBe(false)
await execute()
expect(fetcher).toHaveBeenCalledOnce()
expect(data.value).toEqual(mockData)
expect(error.value).toBeNull()
})
it('handles errors', async () => {
const mockError = new Error('Network error')
const fetcher = vi.fn().mockRejectedValue(mockError)
const onError = vi.fn()
const { data, error, execute } = useAsyncData(fetcher, {
immediate: false,
onError
})
await execute()
expect(error.value).toBe(mockError)
expect(data.value).toBeNull()
expect(onError).toHaveBeenCalledWith(mockError)
})
it('transforms data', async () => {
const fetcher = vi.fn().mockResolvedValue({ users: [{ id: 1 }] })
const transform = (data: any) => data.users
const { data, execute } = useAsyncData(fetcher, {
immediate: false,
transform
})
await execute()
expect(data.value).toEqual([{ id: 1 }])
})
})
// tests/stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '~/stores/user'
// Mock $fetch
vi.stubGlobal('$fetch', vi.fn())
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('logs in user successfully', async () => {
const mockResponse = {
user: { id: '1', email: 'test@test.com', name: 'Test', roles: [] },
token: 'mock-token'
}
vi.mocked($fetch).mockResolvedValue(mockResponse)
const store = useUserStore()
await store.login('test@test.com', 'password')
expect($fetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
body: { email: 'test@test.com', password: 'password' }
})
expect(store.currentUser).toEqual(mockResponse.user)
expect(store.isAuthenticated).toBe(true)
})
it('checks user roles correctly', async () => {
const store = useUserStore()
store.currentUser = {
id: '1',
email: 'admin@test.com',
name: 'Admin',
roles: ['admin', 'user']
}
expect(store.hasRole('admin')).toBe(true)
expect(store.hasRole('superadmin')).toBe(false)
})
it('clears state on logout', async () => {
vi.mocked($fetch).mockResolvedValue({})
const store = useUserStore()
store.currentUser = { id: '1', email: 'test@test.com', name: 'Test', roles: [] }
store.token = 'token'
await store.logout()
expect(store.currentUser).toBeNull()
expect(store.token).toBeNull()
expect(store.isAuthenticated).toBe(false)
})
})
<!-- components/UserCard.vue -->
<script setup lang="ts">
interface User {
id: string
name: string
email: string
}
const props = defineProps<{
user: User | null
loading?: boolean
}>()
const emit = defineEmits<{
select: [id: string]
}>()
const handleClick = () => {
if (props.user) {
emit('select', props.user.id)
}
}
</script>
<template>
<div @click="handleClick" class="user-card">
<div v-if="loading" data-testid="loading-skeleton" class="skeleton">
Loading...
</div>
<template v-else-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</template>
</div>
</template>
# Run all tests
npm run test
# Run tests with coverage
npm run test:coverage
# Run specific test file
npm run test tests/components/UserCard.test.ts
# Type checking
npm run typecheck
# Lint
npm run lint
# Build to ensure no errors
npm run build
Bad - Method called on every render:
<script setup lang="ts">
const items = ref([...])
// ❌ BAD: Recalculates on every render
const getFilteredItems = () => {
return items.value.filter(item => item.active)
}
</script>
<template>
<div v-for="item in getFilteredItems()" :key="item.id">
{{ item.name }}
</div>
</template>
Good - Computed caches result:
<script setup lang="ts">
const items = ref([...])
// ✅ GOOD: Only recalculates when items change
const filteredItems = computed(() => {
return items.value.filter(item => item.active)
})
</script>
<template>
<div v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</div>
</template>
Bad - Deep reactivity on large objects:
// ❌ BAD: Creates deep reactive proxy for entire object
const largeDataset = ref<DataItem[]>([])
// Every nested property becomes reactive
largeDataset.value = await fetchLargeDataset()
Good - Shallow reactivity when deep tracking not needed:
// ✅ GOOD: Only tracks the reference, not nested properties
const largeDataset = shallowRef<DataItem[]>([])
// Manually trigger updates
largeDataset.value = await fetchLargeDataset()
// Use triggerRef for in-place mutations
largeDataset.value.push(newItem)
triggerRef(largeDataset)
Bad - Re-renders all items on any change:
<template>
<!-- ❌ BAD: All items re-render when anything changes -->
<div v-for="item in items" :key="item.id">
<ExpensiveComponent :data="item" />
</div>
</template>
Good - Memoize items that haven't changed:
<template>
<!-- ✅ GOOD: Only re-renders when item.id or item.updated changes -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.updated]"
>
<ExpensiveComponent :data="item" />
</div>
</template>
Bad - All components loaded upfront:
<script setup lang="ts">
// ❌ BAD: Imported even if never shown
import HeavyChart from '~/components/HeavyChart.vue'
import AdminPanel from '~/components/AdminPanel.vue'
import DataTable from '~/components/DataTable.vue'
</script>
Good - Components loaded on demand:
<script setup lang="ts">
// ✅ GOOD: Only loaded when rendered
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
const AdminPanel = defineAsyncComponent({
loader: () => import('~/components/AdminPanel.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
timeout: 5000
})
// With Nuxt lazy prefix
// components/lazy/DataTable.vue automatically becomes lazy
</script>
<template>
<HeavyChart v-if="showChart" />
<AdminPanel v-if="isAdmin" />
<LazyDataTable v-if="showTable" />
</template>
Bad - Render all items at once:
<template>
<!-- ❌ BAD: Renders 10,000 DOM nodes -->
<div v-for="item in tenThousandItems" :key="item.id">
{{ item.name }}
</div>
</template>
Good - Only render visible items:
<script setup lang="ts">
import { useVirtualList } from '@vueuse/core'
const items = ref(generateLargeList(10000))
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 50,
overscan: 5
})
</script>
<template>
<!-- ✅ GOOD: Only renders ~20 visible items -->
<div v-bind="containerProps" class="h-[400px] overflow-auto">
<div v-bind="wrapperProps">
<div v-for="{ data, index } in list" :key="index" class="h-[50px]">
{{ data.name }}
</div>
</div>
</div>
</template>
Bad - Watch entire object unnecessarily:
// ❌ BAD: Triggers on any property change
watch(form, () => {
validateForm()
}, { deep: true })
Good - Watch specific properties:
// ✅ GOOD: Only triggers when email changes
watch(() => form.email, (newEmail) => {
validateEmail(newEmail)
})
// ✅ GOOD: Watch multiple specific props
watch(
[() => form.email, () => form.password],
([email, password]) => {
validateCredentials(email, password)
}
)
Bad - Run on every keystroke:
<script setup lang="ts">
const searchQuery = ref('')
// ❌ BAD: API call on every keystroke
watch(searchQuery, async (query) => {
results.value = await searchAPI(query)
})
</script>
Good - Debounce the operation:
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'
const searchQuery = ref('')
// ✅ GOOD: Wait for user to stop typing
const debouncedSearch = useDebounceFn(async (query: string) => {
results.value = await searchAPI(query)
}, 300)
watch(searchQuery, (query) => {
debouncedSearch(query)
})
</script>
Use composables to extract and reuse logic across components:
// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'
export interface UseAsyncDataOptions<T> {
immediate?: boolean
onError?: (error: Error) => void
transform?: (data: any) => T
}
export function useAsyncData<T>(
fetcher: () => Promise<T>,
options: UseAsyncDataOptions<T> = {}
) {
const { immediate = true, onError, transform } = options
const data: Ref<T | null> = ref(null)
const error: Ref<Error | null> = ref(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
const result = await fetcher()
data.value = transform ? transform(result) : result
} catch (e) {
error.value = e as Error
onError?.(e as Error)
} finally {
loading.value = false
}
}
if (immediate) execute()
return { data, error, loading, execute }
}
Usage:
<script setup lang="ts">
import { useAsyncData } from '~/composables/useAsyncData'
interface User {
id: string
name: string
}
const { data: user, loading, error } = useAsyncData<User>(
() => $fetch('/api/user/me'),
{ immediate: true }
)
</script>
Create strongly-typed stores with composition API:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface User {
id: string
email: string
name: string
roles: string[]
}
export const useUserStore = defineStore('user', () => {
// State
const currentUser = ref<User | null>(null)
const token = ref<string | null>(null)
// Getters
const isAuthenticated = computed(() => !!currentUser.value)
const hasRole = computed(() => (role: string) =>
currentUser.value?.roles.includes(role) ?? false
)
// Actions
async function login(email: string, password: string) {
const response = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: { email, password }
})
currentUser.value = response.user
token.value = response.token
// Persist token
if (process.client) {
localStorage.setItem('auth_token', response.token)
}
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
currentUser.value = null
token.value = null
if (process.client) {
localStorage.removeItem('auth_token')
}
}
async function fetchCurrentUser() {
if (!token.value) return
try {
const user = await $fetch<User>('/api/user/me', {
headers: { Authorization: `Bearer ${token.value}` }
})
currentUser.value = user
} catch (error) {
// Token invalid, clear auth state
await logout()
}
}
return {
currentUser,
token,
isAuthenticated,
hasRole,
login,
logout,
fetchCurrentUser
}
})
Implement authentication and authorization middleware:
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
const userStore = useUserStore()
const publicRoutes = ['/login', '/register', '/forgot-password']
// Allow public routes
if (publicRoutes.includes(to.path)) {
return
}
// Redirect to login if not authenticated
if (!userStore.isAuthenticated) {
return navigateTo('/login', { redirectCode: 401 })
}
// Check role-based access
if (to.meta.requiresAdmin && !userStore.hasRole('admin')) {
return abortNavigation({
statusCode: 403,
message: 'Access denied'
})
}
})
Page with metadata:
<script setup lang="ts">
definePageMeta({
requiresAdmin: true,
layout: 'admin'
})
const users = await useFetch('/api/admin/users')
</script>
Create type-safe API endpoints with input validation:
// server/api/users/[id].post.ts
import { z } from 'zod'
import { createError } from 'h3'
const updateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120).optional()
})
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({
statusCode: 400,
message: 'User ID is required'
})
}
// Validate request body
const body = await readBody(event)
const result = updateUserSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid request data',
data: result.error.format()
})
}
// Check authentication
const session = await requireUserSession(event)
// Check authorization (users can only update themselves unless admin)
if (session.user.id !== id && !session.user.roles.includes('admin')) {
throw createError({
statusCode: 403,
message: 'Not authorized to update this user'
})
}
// Update user in database
const updatedUser = await db.users.update(id, result.data)
return updatedUser
})
Implement strategic code splitting and lazy loading:
<script setup lang="ts">
// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
const AdminPanel = defineAsyncComponent({
loader: () => import('~/components/AdminPanel.vue'),
loadingComponent: () => h('div', 'Loading...'),
delay: 200,
timeout: 3000
})
const showChart = ref(false)
const userStore = useUserStore()
// Only load when needed
const loadChart = () => {
showChart.value = true
}
</script>
<template>
<div>
<button @click="loadChart">Show Chart</button>
<!-- Component only loads when showChart is true -->
<HeavyChart v-if="showChart" :data="chartData" />
<!-- Admin panel only for admins -->
<AdminPanel v-if="userStore.hasRole('admin')" />
</div>
</template>
Nuxt configuration for optimal splitting:
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-ui': ['@headlessui/vue', '@heroicons/vue'],
}
}
}
}
},
experimental: {
payloadExtraction: true, // Extract payload for better caching
componentIslands: true // Islands architecture for partial hydration
}
})
Leverage VueUse composables for robust functionality:
<script setup lang="ts">
import { useLocalStorage, useMediaQuery, useIntersectionObserver } from '@vueuse/core'
import { ref, watch } from 'vue'
// Persistent dark mode
const isDark = useLocalStorage('dark-mode', false)
// Responsive breakpoints
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
// Infinite scroll with intersection observer
const target = ref<HTMLElement | null>(null)
const isVisible = ref(false)
useIntersectionObserver(
target,
([{ isIntersecting }]) => {
isVisible.value = isIntersecting
},
{ threshold: 0.5 }
)
// Load more when target is visible
watch(isVisible, (visible) => {
if (visible && !loading.value) {
loadMore()
}
})
const loadMore = async () => {
// Load more items
}
</script>
<template>
<div :class="{ dark: isDark }">
<button @click="isDark = !isDark">
Toggle {{ isDark ? 'Light' : 'Dark' }} Mode
</button>
<div v-if="isMobile">Mobile View</div>
<div v-else-if="isTablet">Tablet View</div>
<div v-else>Desktop View</div>
<!-- Items list -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- Intersection observer target for infinite scroll -->
<div ref="target" class="loading-trigger">
<span v-if="isVisible">Loading more...</span>
</div>
</div>
</template>
Handle data fetching correctly for SSR/SSG:
<script setup lang="ts">
// ✅ CORRECT: Use Nuxt data fetching composables
// These work on both server and client, with automatic hydration
// Basic fetch
const { data: posts } = await useFetch('/api/posts', {
key: 'posts-list',
transform: (data) => data.posts,
getCachedData: (key) => useNuxtApp().static.data[key]
})
// With params
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`, {
key: `post-${route.params.id}`,
watch: [() => route.params.id] // Refetch when ID changes
})
// With lazy loading (client-side only initially)
const { data: comments, pending } = await useLazyFetch(`/api/posts/${route.params.id}/comments`)
// Using useAsyncData for custom async operations
const { data: userData, refresh } = await useAsyncData(
'user-profile',
async () => {
const [profile, settings] = await Promise.all([
$fetch('/api/user/profile'),
$fetch('/api/user/settings')
])
return { profile, settings }
},
{
server: true, // Fetch on server
lazy: false, // Wait for data before rendering
default: () => ({ profile: null, settings: null })
}
)
// ❌ WRONG: Direct fetch calls will execute twice (server + client)
// const response = await fetch('/api/posts') // Don't do this!
</script>
<template>
<div>
<article v-if="post">
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</article>
<section v-if="!pending">
<h2>Comments ({{ comments?.length || 0 }})</h2>
<div v-for="comment in comments" :key="comment.id">
{{ comment.text }}
</div>
</section>
<div v-else>Loading comments...</div>
</div>
</template>
See references/advanced-patterns.md for more patterns including plugins, modules, and advanced composables.
Risk Level: MEDIUM - Client-side applications are vulnerable to XSS, injection, and data exposure
Risk: Attackers inject malicious scripts through user input, stealing data or performing unauthorized actions.
Prevention:
<script setup lang="ts">
import DOMPurify from 'isomorphic-dompurify'
const userInput = ref('')
const sanitizedHtml = computed(() => DOMPurify.sanitize(userInput.value))
// ✅ SAFE: Vue's template binding automatically escapes HTML
const displayText = ref('<script>alert("XSS")</script>')
</script>
<template>
<!-- ✅ SAFE: Automatic escaping -->
<div>{{ displayText }}</div>
<!-- ⚠️ DANGEROUS: Only use with sanitized content -->
<div v-html="sanitizedHtml"></div>
<!-- ❌ NEVER: Raw user input -->
<!-- <div v-html="userInput"></div> -->
</template>
Risk: Sensitive data leaked through client-side code, API responses, or state management.
Prevention:
// ✅ Server API route - keep secrets on server
// server/api/payment.post.ts
export default defineEventHandler(async (event) => {
const apiKey = useRuntimeConfig().stripeSecretKey // Server-only
const payment = await stripe.charges.create({
amount: 1000,
currency: 'usd',
source: req.body.token
}, {
apiKey // Never exposed to client
})
// Return only necessary data
return {
id: payment.id,
status: payment.status,
amount: payment.amount
}
})
Risk: Attackers trick users into executing unwanted actions on authenticated sessions.
Prevention:
// nuxt.config.ts
export default defineNuxtConfig({
// Enable CSRF protection for SSR
security: {
headers: {
crossOriginEmbedderPolicy: 'require-corp',
crossOriginOpenerPolicy: 'same-origin',
crossOriginResourcePolicy: 'same-origin'
}
}
})
// Middleware for API routes
// server/middleware/csrf.ts
export default defineEventHandler((event) => {
if (event.method !== 'GET' && event.method !== 'HEAD') {
const origin = getHeader(event, 'origin')
const host = getHeader(event, 'host')
if (origin && !origin.includes(host)) {
throw createError({
statusCode: 403,
message: 'CSRF validation failed'
})
}
}
})
| OWASP Category | Relevance | Mitigation in Vue/Nuxt |
|---|---|---|
| A03:2021 Injection | HIGH | Input validation, parameterized queries, sanitization |
| A05:2021 Security Misconfiguration | MEDIUM | CSP headers, secure defaults, environment configs |
| A06:2021 Vulnerable Components | MEDIUM | Regular updates, audit dependencies, Snyk/npm audit |
| A07:2021 Authentication Failures | HIGH | Secure session management, proper token handling |
| A08:2021 Data Integrity Failures | MEDIUM | Signed payloads, integrity checks, HTTPS only |
For detailed security examples and complete OWASP coverage, see references/security-examples.md.
Problem:
// ❌ WRONG: Loses reactivity
const userStore = useUserStore()
const { currentUser } = userStore // Not reactive!
watch(currentUser, () => {
console.log('This will never trigger!')
})
Solution:
// ✅ CORRECT: Preserve reactivity with storeToRefs
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { currentUser } = storeToRefs(userStore) // Reactive!
watch(currentUser, () => {
console.log('This works!')
})
// Or access directly
watch(() => userStore.currentUser, () => {
console.log('This also works!')
})
Problem:
// ❌ WRONG: Event listener not cleaned up
onMounted(() => {
window.addEventListener('resize', handleResize)
})
// Component unmounts but listener persists!
Solution:
// ✅ CORRECT: Clean up in onUnmounted
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// ✅ BETTER: Use VueUse composable
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', handleResize) // Auto cleanup!
Problem:
// ❌ WRONG: useFetch in event handler
const handleClick = async () => {
const { data } = await useFetch('/api/data') // Error! Not allowed in functions
}
// ❌ WRONG: Inside conditional
if (someCondition) {
const { data } = await useFetch('/api/data') // Error! Must be top-level
}
Solution:
// ✅ CORRECT: Use $fetch for programmatic calls
const handleClick = async () => {
const data = await $fetch('/api/data') // Works in functions
}
// ✅ CORRECT: useFetch at component top-level
const { data, refresh } = await useFetch('/api/data', {
immediate: false
})
const handleClick = () => {
refresh() // Trigger refetch
}
Problem:
// ❌ WRONG: Accessing browser APIs during SSR
const windowWidth = ref(window.innerWidth) // Error! window undefined on server
onMounted(() => {
localStorage.setItem('key', 'value') // Error! localStorage undefined on server
})
Solution:
// ✅ CORRECT: Check environment
const windowWidth = ref(0)
onMounted(() => {
if (process.client) {
windowWidth.value = window.innerWidth
}
})
// ✅ BETTER: Use VueUse with SSR safety
import { useWindowSize, useLocalStorage } from '@vueuse/core'
const { width } = useWindowSize() // SSR-safe
const stored = useLocalStorage('key', 'default') // SSR-safe
Problem:
// ❌ WRONG: Watching entire object (triggers on any property change)
const form = reactive({
name: '',
email: '',
phone: '',
address: ''
})
watch(form, () => {
console.log('Triggers for ANY field change!')
})
Solution:
// ✅ CORRECT: Watch specific properties
watch(() => form.email, (newEmail) => {
validateEmail(newEmail)
})
// ✅ CORRECT: Watch multiple specific properties
watch([() => form.email, () => form.phone], ([email, phone]) => {
validateContactInfo(email, phone)
})
// ✅ CORRECT: Deep watch with immediate flag when needed
watch(form, () => {
saveFormDraft(form)
}, {
deep: true,
debounce: 500 // Debounce to avoid excessive calls
})
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./tests/setup.ts'],
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'tests/']
}
},
resolve: {
alias: {
'~': resolve(__dirname, './')
}
}
})
// tests/setup.ts
import { config } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
// Global plugins
config.global.plugins = [createTestingPinia()]
// Mock Nuxt composables
vi.mock('#app', () => ({
useNuxtApp: () => ({ $fetch: vi.fn() }),
useRuntimeConfig: () => ({ public: {} }),
useFetch: vi.fn(),
useAsyncData: vi.fn(),
navigateTo: vi.fn(),
definePageMeta: vi.fn()
}))
// Mock $fetch globally
vi.stubGlobal('$fetch', vi.fn())
// tests/components/Form.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import Form from '~/components/Form.vue'
describe('Form', () => {
it('validates required fields', async () => {
const wrapper = mount(Form)
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error').text()).toContain('Name is required')
})
it('submits valid data', async () => {
const onSubmit = vi.fn()
const wrapper = mount(Form, {
props: { onSubmit }
})
await wrapper.find('input[name="name"]').setValue('John')
await wrapper.find('input[name="email"]').setValue('john@test.com')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(onSubmit).toHaveBeenCalledWith({
name: 'John',
email: 'john@test.com'
})
})
it('shows loading state during submission', async () => {
const wrapper = mount(Form, {
props: {
onSubmit: () => new Promise(r => setTimeout(r, 100))
}
})
await wrapper.find('input[name="name"]').setValue('John')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('button').attributes('disabled')).toBeDefined()
expect(wrapper.find('.loading').exists()).toBe(true)
})
})
// tests/composables/useApi.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { useApi } from '~/composables/useApi'
describe('useApi', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('handles concurrent requests', async () => {
const results = ['first', 'second']
let callCount = 0
vi.mocked($fetch).mockImplementation(() =>
Promise.resolve(results[callCount++])
)
const { data, execute } = useApi('/api/test')
// Fire two requests
execute()
execute()
await flushPromises()
// Should have latest result
expect(data.value).toBe('second')
})
it('cancels pending request on new request', async () => {
const abortSpy = vi.fn()
vi.mocked($fetch).mockImplementation((_, opts) => {
opts?.signal?.addEventListener('abort', abortSpy)
return new Promise(() => {})
})
const { execute } = useApi('/api/test')
execute()
execute() // Should cancel first
expect(abortSpy).toHaveBeenCalled()
})
})
tsconfig.jsondefineProps<T>() syntaxnuxt prepareuseFetch/useAsyncData for data fetching (SSR-compatible)defineAsyncComponent()nuxi analyze and set budgetsv-memo for expensive lists that don't change oftenv-html with unsanitized user inputnuxt.config.ts.env files, never in client codehttpOnly cookies for sensitive tokensstoreToRefs() to maintain reactivityprocess.client before accessing browser APIsrouteRules for page-level rendering strategy<ClientOnly> when neededprocess.client before browser APIsnpm run testnpm run typechecknpm run lintnpm run build# Run all checks before commit
npm run test && npm run typecheck && npm run lint && npm run build
# Quick verification during development
npm run dev # Should start without errors
# Full test suite with coverage
npm run test:coverage
# E2E tests
npm run test:e2e
This skill provides expertise in building modern, performant, type-safe Vue 3 and Nuxt 3 applications. Key takeaways:
Architecture: Design component hierarchies with Composition API, extract logic into composables, and manage state with Pinia. Follow the composable-first approach for maximum reusability.
Nuxt 3 Patterns: Leverage file-based routing, auto-imports, and Nitro server for full-stack development. Configure rendering strategies (SSR/SSG/hybrid) per route for optimal performance.
Type Safety: Use TypeScript strict mode throughout. Type components, stores, and API responses. Combine compile-time TypeScript with runtime validation (Zod) for robust applications.
Performance: Implement strategic code splitting, lazy loading, and optimized data fetching with useFetch. Monitor Core Web Vitals and set performance budgets.
Security: Prevent XSS through proper escaping and sanitization. Validate all inputs. Configure CSP headers. Keep secrets on the server. Implement CSRF protection.
Common Pitfalls: Preserve reactivity with storeToRefs. Clean up event listeners. Use correct data fetching APIs (useFetch vs $fetch). Handle SSR/client differences. Write efficient watchers.
Best Practices:
Risk Level: MEDIUM - Primary concerns are client-side security (XSS, data exposure) and performance (bundle size, SSR complexity).
For advanced patterns, see references/advanced-patterns.md. For detailed security examples, see references/security-examples.md.