All skills
jeffallan avatar

/vue-expert

@efebc44
by jeffallanjeffallan/claude-skills12k stars
1,124

Builds Vue 3 components with Composition API patterns, configures Nuxt 3 SSR/SSG projects, sets up Pinia stores, scaffolds Quasar/Capacitor mobile apps, implements PWA features, and optimises Vite builds. Use when creating Vue 3 applications with Composition API, writing reusable composables, managing state with Pinia, building hybrid mobile apps with Quasar or Capacitor, configuring service workers, or tuning Vite configuration and TypeScript integration.

Use this Skill: https://skilld.dev/gh/jeffallan/claude-skills/vue-expert

This session only. Nothing lands on disk.

referencesnuxt.md

≈3.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Nuxt 3

Project Structure

my-nuxt-app/
├── app.vue              # Root component (optional)
├── nuxt.config.ts       # Nuxt configuration
├── package.json
├── tsconfig.json
├── .output/             # Build output
├── assets/              # Uncompiled assets (CSS, images)
├── public/              # Static files (served at root)
├── components/          # Auto-imported components
│   ├── AppHeader.vue
│   └── base/
│       └── Button.vue   # Used as <BaseButton>
├── composables/         # Auto-imported composables
│   └── useAuth.ts
├── layouts/             # Layout components
│   ├── default.vue
│   └── admin.vue
├── middleware/          # Route middleware
│   └── auth.ts
├── pages/               # File-based routing
│   ├── index.vue        # /
│   ├── about.vue        # /about
│   ├── users/
│   │   ├── index.vue    # /users
│   │   └── [id].vue     # /users/:id
│   └── [...slug].vue    # Catch-all route
├── plugins/             # Plugins
│   └── api.ts
├── server/              # Server API routes
│   ├── api/
│   │   └── users.ts     # /api/users
│   └── middleware/
│       └── log.ts
└── stores/              # Pinia stores
    └── user.ts

File-based Routing

<!-- pages/index.vue -->
<script setup lang="ts">
definePageMeta({
  title: 'Home',
  layout: 'default'
})
</script>

<template>
  <div>
    <h1>Home Page</h1>
  </div>
</template>

<!-- pages/about.vue -->
<template>
  <div>About Page</div>
</template>

<!-- pages/users/[id].vue - Dynamic route -->
<script setup lang="ts">
const route = useRoute()
const userId = computed(() => route.params.id)

const { data: user } = await useFetch(`/api/users/${userId.value}`)
</script>

<template>
  <div>
    <h1>User: {{ user?.name }}</h1>
  </div>
</template>

<!-- pages/blog/[...slug].vue - Catch-all route -->
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug // ['2024', '12', 'my-post']
</script>

<template>
  <div>Blog post: {{ slug }}</div>
</template>

Layouts

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

<!-- layouts/admin.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth' // Protect with middleware
})
</script>

<template>
  <div class="admin-layout">
    <aside>Admin Sidebar</aside>
    <main>
      <slot />
    </main>
  </div>
</template>

<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  layout: 'admin'
})
</script>

<template>
  <div>Admin Dashboard</div>
</template>

Data Fetching

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

// useFetch - SSR-safe, auto-imports
const { data: users, pending, error, refresh } = await useFetch<User[]>('/api/users')

// With options
const { data } = await useFetch('/api/users', {
  method: 'POST',
  body: { name: 'John' },
  headers: {
    'Authorization': 'Bearer token'
  },
  query: { page: 1, limit: 10 },
  // Transform response
  transform: (data) => data.map(u => ({ ...u, fullName: u.firstName + ' ' + u.lastName })),
  // Pick specific keys
  pick: ['id', 'name'],
  // Watch for changes
  watch: [page, limit]
})

// useAsyncData - More control
const { data: user } = await useAsyncData(
  'user-123', // Unique key for caching
  async () => {
    const response = await fetch('/api/users/123')
    return response.json()
  },
  {
    server: true, // Fetch on server
    lazy: false, // Don't block navigation
    default: () => null // Default value while loading
  }
)

// useLazyFetch - Non-blocking
const { data: posts } = await useLazyFetch('/api/posts')

// useLazyAsyncData - Non-blocking with custom fetcher
const { data: comments } = await useLazyAsyncData('comments', () =>
  $fetch('/api/comments')
)

// Manual refresh
function handleRefresh() {
  refresh() // Re-fetch data
}
</script>

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>
      <div v-for="user in users" :key="user.id">
        {{ user.name }}
      </div>
      <button @click="handleRefresh">Refresh</button>
    </div>
  </div>
</template>

Server API Routes

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const limit = Number(query.limit) || 10

  // Fetch from database
  const users = await prisma.user.findMany({
    skip: (page - 1) * limit,
    take: limit
  })

  return users
})

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

  const user = await prisma.user.findUnique({
    where: { id: Number(id) }
  })

  if (!user) {
    throw createError({
      statusCode: 404,
      message: 'User not found'
    })
  }

  return user
})

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

  // Validate
  if (!body.email || !body.name) {
    throw createError({
      statusCode: 400,
      message: 'Email and name are required'
    })
  }

  const user = await prisma.user.create({
    data: {
      email: body.email,
      name: body.name
    }
  })

  return user
})

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event)

  // Verify credentials
  const user = await verifyCredentials(email, password)

  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Invalid credentials'
    })
  }

  // Set session cookie
  setCookie(event, 'session', user.sessionToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7 // 7 days
  })

  return { success: true, user }
})

Middleware

// middleware/auth.ts - Route middleware
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuthStore()

  if (!isLoggedIn) {
    return navigateTo('/login')
  }
})

// middleware/logger.global.ts - Global middleware
export default defineNuxtRouteMiddleware((to, from) => {
  console.log(`Navigating from ${from.path} to ${to.path}`)
})

// server/middleware/log.ts - Server middleware
export default defineEventHandler((event) => {
  console.log(`[${event.method}] ${event.path}`)
})

Composables

// composables/useAuth.ts - Auto-imported
export const useAuth = () => {
  const user = useState<User | null>('user', () => null)
  const isLoggedIn = computed(() => user.value !== null)

  async function login(email: string, password: string) {
    const { data, error } = await useFetch('/api/auth/login', {
      method: 'POST',
      body: { email, password }
    })

    if (data.value) {
      user.value = data.value.user
    }

    return { data, error }
  }

  async function logout() {
    await useFetch('/api/auth/logout', { method: 'POST' })
    user.value = null
    navigateTo('/login')
  }

  async function fetchUser() {
    const { data } = await useFetch('/api/auth/me')
    user.value = data.value
  }

  return {
    user,
    isLoggedIn,
    login,
    logout,
    fetchUser
  }
}

// Usage in component (auto-imported)
<script setup lang="ts">
const { user, isLoggedIn, login, logout } = useAuth()
</script>

Plugins

// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
  const api = $fetch.create({
    baseURL: '/api',
    onRequest({ options }) {
      // Add auth token
      const token = useCookie('token')
      if (token.value) {
        options.headers = options.headers || {}
        options.headers.Authorization = `Bearer ${token.value}`
      }
    },
    onResponseError({ response }) {
      if (response.status === 401) {
        navigateTo('/login')
      }
    }
  })

  return {
    provide: {
      api
    }
  }
})

// Usage in component
<script setup lang="ts">
const { $api } = useNuxtApp()
const users = await $api('/users')
</script>

Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },

  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss',
    '@vueuse/nuxt'
  ],

  runtimeConfig: {
    // Server-only (never exposed to client)
    apiSecret: process.env.API_SECRET,

    // Exposed to client
    public: {
      apiBase: process.env.API_BASE || '/api'
    }
  },

  app: {
    head: {
      title: 'My App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'My amazing site' }
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
      ]
    }
  },

  css: ['~/assets/css/main.css'],

  typescript: {
    strict: true,
    typeCheck: true
  },

  // Vite is the default bundler in Nuxt 3
  // Note: webpack is deprecated - use Vite for all new projects
  vite: {
    optimizeDeps: {
      include: ['vue', 'vue-router', 'pinia']
    },
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vendor': ['vue', 'pinia']
          }
        }
      }
    }
  },

  nitro: {
    preset: 'vercel' // or 'node-server', 'cloudflare', 'bun', etc.
  }
})

SEO and Meta Tags

<script setup lang="ts">
const route = useRoute()
const title = computed(() => `User ${route.params.id}`)

useHead({
  title,
  meta: [
    { name: 'description', content: 'User profile page' },
    { property: 'og:title', content: title },
    { property: 'og:description', content: 'User profile' }
  ]
})

// Or use useSeoMeta
useSeoMeta({
  title: 'My Page',
  ogTitle: 'My Page',
  description: 'Page description',
  ogDescription: 'Page description',
  ogImage: 'https://example.com/image.png'
})
</script>

Custom SSR with Fastify (Non-Nuxt)

For custom Vue 3 SSR without Nuxt, using Fastify as the server:

// server.ts
import Fastify from 'fastify'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'

const fastify = Fastify({ logger: true })

fastify.get('*', async (request, reply) => {
  const app = createSSRApp(App)

  // Server-side data fetching
  const initialState = await fetchInitialData(request.url)

  const html = await renderToString(app)

  reply.type('text/html').send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR</title>
        <script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script>
      </head>
      <body>
        <div id="app">${html}</div>
        <script type="module" src="/src/entry-client.ts"></script>
      </body>
    </html>
  `)
})

fastify.listen({ port: 3000 })
// entry-client.ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// Hydrate with server state
if (window.__INITIAL_STATE__) {
  app.provide('initialState', window.__INITIAL_STATE__)
}

app.mount('#app')
// vite.config.ts for SSR
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    ssr: true,
    rollupOptions: {
      input: {
        server: './server.ts',
        client: './src/entry-client.ts'
      }
    }
  }
})

Hydration Patterns

Lazy Hydration with ClientOnly

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// Heavy component loaded only on client
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)
</script>

<template>
  <ClientOnly>
    <HeavyChart />
    <template #fallback>
      <div class="chart-skeleton">Loading chart...</div>
    </template>
  </ClientOnly>
</template>

Hydration Mismatch Prevention

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// Avoid hydration mismatch for client-only values
const currentTime = ref<string | null>(null)
const windowWidth = ref<number | null>(null)

onMounted(() => {
  // These values differ between server and client
  currentTime.value = new Date().toLocaleTimeString()
  windowWidth.value = window.innerWidth
})
</script>

<template>
  <div>
    <!-- Use v-if to prevent mismatch -->
    <span v-if="currentTime">{{ currentTime }}</span>
    <span v-else>--:--:--</span>

    <!-- Or use ClientOnly -->
    <ClientOnly>
      <span>Width: {{ windowWidth }}px</span>
    </ClientOnly>
  </div>
</template>

Progressive Hydration

<script setup lang="ts">
// Use nuxt-delay-hydration for non-critical content
definePageMeta({
  // Delay hydration until visible or idle
  hydration: 'when-visible' // or 'on-idle'
})
</script>

<template>
  <div>
    <!-- Critical content hydrates immediately -->
    <header>Navigation</header>

    <!-- Non-critical content can wait -->
    <LazyBelowFoldContent />
  </div>
</template>
// nuxt.config.ts - Configure delay hydration
export default defineNuxtConfig({
  modules: ['nuxt-delay-hydration'],

  delayHydration: {
    mode: 'init', // or 'mount'
    debug: process.env.NODE_ENV === 'development'
  }
})

Quick Reference

Pattern Use Case
useFetch() Fetch data (SSR-safe)
useAsyncData() Custom async operations
useLazyFetch() Non-blocking fetch
useState() Shared state across components
useRoute() Access route params/query
useRouter() Navigate programmatically
navigateTo() Navigate to route
definePageMeta() Page-level metadata
useHead() Dynamic meta tags
Server routes /server/api/*.ts
Auto-imports Components, composables, utils
<ClientOnly> Client-only rendering, prevent hydration mismatch
renderToString() Custom SSR with Fastify/Express
vite: {} Vite configuration in nuxt.config.ts
nuxt-delay-hydration Progressive hydration for performance

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub16d

    The skill provides comprehensive documentation and boilerplate for Vue 3, Nuxt 3, and hybrid mobile development using modern best practices. It guides the agent through standard development workflows including type checking, unit testing, and build optimization using community-standard tools. All external references target the author's own documentation hosted on GitHub Pages, and all package dependencies are well-known industry libraries.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    1/8 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at efebc44. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 5 months ago
Other metadata
metadata
{
  "author": "https://github.com/Jeffallan",
  "version": "1.1.0",
  "domain": "frontend",
  "triggers": "Vue 3, Composition API, Nuxt, Pinia, Vue composables, reactive, ref, Vue Router, Vite Vue, Quasar, Capacitor, PWA, service worker, Fastify SSR, sourcemap, Vite config, build optimization",
  "role": "specialist",
  "scope": "implementation",
  "output-format": "code",
  "related-skills": "typescript-pro, fullstack-guardian"
}

README badge

README badge for jeffallan/claude-skills/vue-expert