CursorPool
← 返回首页

vue-nuxt-skills

vue-nuxt-skills plugin for Cursor

cursor.directory·4
Skill

nuxt3

# Nuxt 3 Skill ## Description Use this skill when the task involves **building or modifying a Nuxt 3 application** — including pages, layouts, server API routes, middleware, plugins, SEO, data fet

# Nuxt 3 Skill

## Description
Use this skill when the task involves **building or modifying a Nuxt 3 application** — including pages, layouts, server API routes, middleware, plugins, SEO, data fetching, SSR/SSG configuration, or Nuxt modules.

Trigger phrases: "create a page", "add an API route", "set up server route", "configure Nuxt", "add SEO meta", "create middleware", "set up auth", "add a layout", "configure rendering", "write a Nuxt plugin", "fetch data in Nuxt", "create a Nitro route", "add a Nuxt module".

---

## How to Use This Skill

When working on a Nuxt 3 task, follow this workflow:

1. **Identify the rendering requirement first** — SSR, SSG, ISR, or CSR?
2. **Determine where data lives** — server route, external API, or static?
3. **Choose the right data fetching primitive** — `useFetch`, `useAsyncData`, or `$fetch`
4. **Implement server route if needed** — validate input, handle errors, return typed data
5. **Build the page/component** — use auto-imports, no manual imports for Nuxt utils
6. **Add SEO meta** — always use `useSeoMeta()`
7. **Apply middleware** — auth, redirects, analytics

---

## Project Structure Reference

```
.
├── app.vue                        # Global app wrapper (optional)
├── nuxt.config.ts                 # Central config
├── error.vue                      # Custom error page
│
├── pages/
│   ├── index.vue                  # → /
│   ├── about.vue                  # → /about
│   ├── blog/
│   │   ├── index.vue              # → /blog
│   │   └── [slug].vue             # → /blog/:slug
│   └── users/
│       ├── index.vue              # → /users
│       ├── [id]/
│       │   ├── index.vue          # → /users/:id
│       │   └── settings.vue      # → /users/:id/settings
│       └── [...slug].vue          # → /users/* (catch-all)
│
├── components/
│   ├── ui/                        # Base: Button, Input, Modal
│   ├── App/                       # App-level: AppHeader, AppFooter
│   └── [Feature]/                 # Feature-specific components
│
├── composables/                   # Auto-imported useXxx.ts files
├── utils/                         # Auto-imported utility functions
│
├── layouts/
│   ├── default.vue                # Default layout
│   └── dashboard.vue              # Dashboard layout
│
├── middleware/
│   ├── auth.ts                    # Named: requiresAuth pages
│   └── redirect.global.ts        # Global: runs on every navigation
│
├── plugins/
│   ├── analytics.client.ts        # Client-only plugin
│   └── error-handler.ts          # Universal plugin
│
├── server/
│   ├── api/                       # → /api/* routes
│   │   ├── users/
│   │   │   ├── index.get.ts      # GET  /api/users
│   │   │   ├── index.post.ts     # POST /api/users
│   │   │   └── [id].get.ts      # GET  /api/users/:id
│   │   └── health.get.ts        # GET  /api/health
│   ├── routes/                    # Custom non-/api/* routes
│   ├── middleware/                # Server middleware (runs every request)
│   └── utils/                    # Server-only shared utilities (auto-imported)
│
├── stores/                        # Pinia stores
└── types/                         # Shared TypeScript types
```

---

## `nuxt.config.ts` Skill

### Full production-ready config template

```ts
export default defineNuxtConfig({
  devtools: { enabled: true },

  // --- Modules ---
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt',
    '@nuxt/image',
    '@vueuse/nuxt',
    '@nuxtjs/i18n',
    'nuxt-security',
    '@nuxt/content',       // If using CMS/markdown
  ],

  // --- Runtime Config ---
  runtimeConfig: {
    // 🔒 Server-only (never sent to browser)
    databaseUrl: process.env.DATABASE_URL,
    jwtSecret: process.env.JWT_SECRET,
    stripeSecretKey: process.env.STRIPE_SECRET_KEY,
    // 🌐 Public (exposed to browser — safe values only)
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE ?? '/api',
      appName: process.env.NUXT_PUBLIC_APP_NAME ?? 'My App',
      sentryDsn: process.env.NUXT_PUBLIC_SENTRY_DSN,
    },
  },

  // --- Rendering Strategy Per Route ---
  routeRules: {
    '/':                  { prerender: true },          // Static home
    '/about':             { prerender: true },          // Static page
    '/blog/**':           { isr: 3600 },               // ISR — 1hr revalidation
    '/docs/**':           { prerender: true },          // Full static
    '/dashboard/**':      { ssr: false },               // SPA — client-only
    '/admin/**':          { ssr: false },               // SPA — client-only
    '/api/**':            { cors: true, cache: false }, // API routes
  },

  // --- TypeScript ---
  typescript: {
    strict: true,
    typeCheck: true,
  },

  // --- App Head Defaults ---
  app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
    },
  },
})
```

---

## Data Fetching Skill

### Decision tree — which primitive to use?

```
Is this inside a Vue component or page?
  ├─ YES → Is it needed for SSR (visible on first load)?
  │         ├─ YES → useFetch() or useAsyncData()
  │         └─ NO  → $fetch() inside onMounted() or with lazy: true
  └─ NO (inside event handler / store action / server route)
           └─ $fetch()
```

### `useFetch` — standard SSR data fetching

```ts
// Basic
const { data, status, error, refresh } = await useFetch<User[]>('/api/users')

// With options
const { data: user } = await useFetch<User>(`/api/users/${route.params.id}`, {
  // Cache key — must be unique per fetch
  key: `user-${route.params.id}`,
  // Re-fetch when this reactive value changes
  watch: [() => route.params.id],
  // Transform response
  transform: (res) => res.data,
  // Don't block navigation — load in background
  lazy: true,
  // Pass auth header
  headers: { Authorization: `Bearer ${token.value}` },
})

// Always handle status
if (status.value === 'error') {
  // handle error.value
}
```

### `useAsyncData` — multi-source or custom logic

```ts
const { data } = await useAsyncData('dashboard', async () => {
  const [stats, recentOrders, topProducts] = await Promise.all([
    $fetch<Stats>('/api/dashboard/stats'),
    $fetch<Order[]>('/api/orders/recent'),
    $fetch<Product[]>('/api/products/top'),
  ])
  return { stats, recentOrders, topProducts }
}, {
  watch: [selectedDateRange],
})
```

### `$fetch` — mutations and event handlers

```ts
// In component
async function createPost() {
  const post = await $fetch<Post>('/api/posts', {
    method: 'POST',
    body: {
      title: form.title,
      content: form.content,
    },
  })
  await navigateTo({ name: 'PostDetail', params: { id: post.id } })
}
```

---

## Server Routes Skill

### File naming convention

| Filename | HTTP Method | URL |
|---|---|---|
| `users/index.get.ts` | GET | `/api/users` |
| `users/index.post.ts` | POST | `/api/users` |
| `users/[id].get.ts` | GET | `/api/users/:id` |
| `users/[id].patch.ts` | PATCH | `/api/users/:id` |
| `users/[id].delete.ts` | DELETE | `/api/users/:id` |

### Full server route template

```ts
// server/api/posts/index.post.ts
import { z } from 'zod'

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).optional().default([]),
  published: z.boolean().default(false),
})

export default defineEventHandler(async (event) => {
  // 1. Authenticate
  const user = await requireAuth(event) // from server/utils/auth.ts

  // 2. Read and validate body
  const rawBody = await readBody(event)
  const result = CreatePostSchema.safeParse(rawBody)

  if (!result.success) {
    throw createError({
      statusCode: 422,
      message: 'Validation failed',
      data: result.error.flatten(),
    })
  }

  const body = result.data

  // 3. Business logic
  const post = await db.post.create({
    data: {
      ...body,
      authorId: user.id,
    },
  })

  // 4. Return — Nitro auto-serializes
  return post
})
```

### Reading route params, query, and body

```ts
export default defineEventHandler(async (event) => {
  // Route param from [id].get.ts
  const id = getRouterParam(event, 'id')

  // Query string ?page=1&limit=20
  const query = getQuery(event)
  const page = Number(query.page ?? 1)
  const limit = Number(query.limit ?? 20)

  // Request body (POST/PATCH)
  const body = await readBody(event)

  // Headers
  const authHeader = getHeader(event, 'authorization')

  // Cookies
  const sessionToken = getCookie(event, 'session')
})
```

### Server utility (shared across routes)

```ts
// server/utils/auth.ts — auto-imported in server routes
import type { H3Event } from 'h3'

export async function requireAuth(event: H3Event) {
  const token = getCookie(event, 'auth-token')
    ?? getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (!token) {
    throw createError({ statusCode: 401, message: 'Authentication required' })
  }

  try {
    const config = useRuntimeConfig()
    const payload = verifyJwt(token, config.jwtSecret)
    return payload
  } catch {
    throw createError({ statusCode: 401, message: 'Invalid or expired token' })
  }
}
```

---

## Pages & Layouts Skill

### Page template with all features

```vue
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
// 1. Page meta (static)
definePageMeta({
  name: 'BlogPost',
  layout: 'blog',
  middleware: ['auth'],
})

// 2. Route
const route = useRoute()
const slug = computed(() => route.params.slug as string)

// 3. Data fetching
const { data: post, status } = await useFetch<Post>(`/api/posts/${slug.value}`, {
  key: `post-${slug.value}`,
})

// 4. Handle not found
if (!post.value) {
  throw createError({ statusCode: 404, message: 'Post not found' })
}

// 5. SEO
useSeoMeta({
  title: post.value.title,
  description: post.value.excerpt,
  ogTitle: post.value.title,
  ogDescription: post.value.excerpt,
  ogImage: post.value.coverImage,
  ogType: 'article',
  twitterCard: 'summary_large_image',
})
</script>

<template>
  <div v-if="status === 'pending'">
    <PostSkeleton />
  </div>
  <article v-else-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.renderedContent" />
  </article>
</template>
```

### Layout template

```vue
<!-- layouts/dashboard.vue -->
<script setup lang="ts">
const authStore = useAuthStore()
const { user } = storeToRefs(authStore)
</script>

<template>
  <div class="dashboard-layout">
    <AppSidebar />
    <main class="dashboard-main">
      <AppTopbar :user="user" />
      <div class="dashboard-content">
        <slot /> <!-- Pages render here -->
      </div>
    </main>
  </div>
</template>
```

---

## Middleware Skill

### Named middleware (opt-in per page)

```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isAuthenticated, user } = storeToRefs(useAuthStore())

  if (!isAuthenticated.value) {
    return navigateTo({
      name: 'Login',
      query: { redirect: to.fullPath },
    })
  }

  // Role check
  if (to.meta.requiredRole && user.value?.role !== to.meta.requiredRole) {
    return abortNavigation(
      createError({ statusCode: 403, message: 'Forbidden' }),
    )
  }
})
```

### Global middleware (runs on every navigation)

```ts
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to) => {
  // Runs automatically on every route change
  if (import.meta.client) {
    trackPageView(to.fullPath)
  }
})
```

---

## Plugin Skill

```ts
// plugins/toast.client.ts — client-only
import Toast from 'vue-toastification'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Toast, {
    position: 'top-right',
    timeout: 3000,
  })

  // Provide typed helper
  nuxtApp.provide('toast', {
    success: (msg: string) => useToast().success(msg),
    error: (msg: string) => useToast().error(msg),
    info: (msg: string) => useToast().info(msg),
  })
})

// Augment types
declare module '#app' {
  interface NuxtApp {
    $toast: {
      success(msg: string): void
      error(msg: string): void
      info(msg: string): void
    }
  }
}
```

---

## SEO Skill

### Global defaults in `app.vue`

```ts
// app.vue
useSeoMeta({
  titleTemplate: '%s | My Brand',
  description: 'Default site description for social sharing',
  ogSiteName: 'My Brand',
  ogImage: 'https://mysite.com/og-default.png',
  twitterCard: 'summary_large_image',
  twitterSite: '@mybrand',
})
```

### Dynamic SEO per page

```ts
// Computed SEO from fetched data
watchEffect(() => {
  if (product.value) {
    useSeoMeta({
      title: product.value.name,
      description: product.value.shortDescription,
      ogImage: product.value.images[0]?.url,
      ogType: 'product',
    })
  }
})
```

### Structured data (JSON-LD)

```ts
useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: post.value.title,
        author: { '@type': 'Person', name: post.value.author.name },
        datePublished: post.value.publishedAt,
      }),
    },
  ],
})
```

---

## Rendering Strategy Reference

| Use Case | Strategy | Config |
|---|---|---|
| Marketing pages | Full static | `prerender: true` |
| Blog / docs | ISR (hourly) | `isr: 3600` |
| Product pages | ISR (15 min) | `isr: 900` |
| Dashboard / app | Client-only SPA | `ssr: false` |
| Default (dynamic) | SSR per request | (default, no config needed) |
| API routes | No cache | `cache: false` |

---

## Common Anti-Patterns to Avoid

| ❌ Wrong | ✅ Correct |
|---|---|
| `import { useFetch } from '#app'` | Just use `useFetch` — it's auto-imported |
| `import { ref } from 'vue'` | Auto-imported — remove the import |
| `window.localStorage` in `<script setup>` | Wrap in `onMounted` or use `.client.ts` plugin |
| `process.env.SECRET` in component | Use `useRuntimeConfig().public.xxx` (public only) |
| `fetch('/api/users')` in `<script setup>` | Use `useFetch('/api/users')` for SSR |
| `onMounted(() => { fetch data })` for SSR | Use `useFetch` or `useAsyncData` instead |
| `<a href="/about">` for internal links | Use `<NuxtLink to="/about">` |
| `router.push()` before `navigateTo()` | Use `navigateTo()` — Nuxt-aware navigation |
Skill

vue3

# Vue 3 Skill ## Description Use this skill when the task involves **creating, editing, or reviewing Vue 3 components, composables, Pinia stores, or Vue Router configuration**. This skill provides

# Vue 3 Skill

## Description
Use this skill when the task involves **creating, editing, or reviewing Vue 3 components, composables, Pinia stores, or Vue Router configuration**. This skill provides deep expertise in Vue 3 Composition API patterns, component architecture, state management, and performance optimization.

Trigger phrases: "create a component", "write a composable", "set up Pinia", "build a form", "add Vue Router", "make it reactive", "refactor to Composition API", "build a modal", "create a reusable component", "add a store", "should I use Pinia or TanStack Query", "set up TanStack Query", "axios vs tanstack", "when to use Pinia", "state management in Vue", "cache API data", "data fetching setup".

---

## How to Use This Skill

When working on Vue 3 tasks, follow this workflow:

1. **Understand the component's responsibility** — one component = one job
2. **Identify what state is needed** — local `ref`/`reactive`, composable, Pinia (client state), or TanStack Query (server state) — see State Management Decision Guide below
3. **Identify what the parent needs to know** — design the `emit` interface first
4. **Write `<script setup>` before the template** — logic drives structure
5. **Write the template to match the logic** — not the other way around
6. **Add scoped styles last** — no global leakage

---

## Component Skeleton

Every Vue 3 component must follow this exact structure:

```vue
<script setup lang="ts">
// 1. defineOptions (name for DevTools)
defineOptions({ name: 'ComponentName' })

// 2. Props interface + defineProps
interface Props {
  title: string
  count?: number
  isDisabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  isDisabled: false,
})

// 3. Emits
const emit = defineEmits<{
  submit: [value: string]
  cancel: []
  update: [field: string, value: unknown]
}>()

// 4. Injected dependencies (useRoute, useRouter, stores)
const router = useRouter()
const authStore = useAuthStore()

// 5. Local reactive state
const isOpen = ref(false)
const inputValue = ref('')

// 6. Computed values
const isValid = computed(() => inputValue.value.trim().length > 0)
const displayTitle = computed(() => props.title.toUpperCase())

// 7. Composables
const { user, isLoading } = useUser(props.userId)

// 8. Methods / handlers
function handleSubmit() {
  if (!isValid.value) return
  emit('submit', inputValue.value)
}

// 9. Lifecycle hooks
onMounted(() => {
  // DOM is available here
})

onUnmounted(() => {
  // cleanup here
})
</script>

<template>
  <div class="component-root">
    <!-- template content -->
  </div>
</template>

<style scoped>
.component-root {
  /* scoped styles only */
}
</style>
```

---

## Composable Skill

### When to extract a composable
- Logic is used in 2+ components → extract immediately
- Logic has its own loading/error state → extract always
- Logic involves async operations → extract always
- Logic involves event listeners or timers → extract and clean up

### Composable template

```ts
// composables/useResourceName.ts

import type { MaybeRef } from 'vue'

interface ResourceNameOptions {
  immediate?: boolean
  onError?: (err: Error) => void
}

export function useResourceName(
  id: MaybeRef<string>,
  options: ResourceNameOptions = {},
) {
  const { immediate = true, onError } = options

  // State
  const data = ref<ResourceType | null>(null)
  const isLoading = ref(false)
  const error = ref<Error | null>(null)

  // Core async function
  async function fetch() {
    if (!toValue(id)) return

    isLoading.value = true
    error.value = null

    try {
      data.value = await apiClient.get<ResourceType>(`/resource/${toValue(id)}`)
    } catch (err) {
      const e = err instanceof Error ? err : new Error(String(err))
      error.value = e
      onError?.(e)
    } finally {
      isLoading.value = false
    }
  }

  async function update(payload: Partial<ResourceType>) {
    try {
      data.value = await apiClient.patch(`/resource/${toValue(id)}`, payload)
    } catch (err) {
      error.value = err instanceof Error ? err : new Error(String(err))
    }
  }

  // Reactive refetch when id changes
  watch(() => toValue(id), fetch, { immediate })

  return {
    data: readonly(data),
    isLoading: readonly(isLoading),
    error: readonly(error),
    fetch,
    update,
  }
}
```

### Key composable rules
- Return `readonly()` wrapped refs to prevent accidental mutation from outside
- Accept `MaybeRef<T>` for params so callers can pass either a `ref` or a raw value
- Use `toValue()` (Vue 3.3+) inside the composable to unwrap either
- Always clean up event listeners, timers, subscriptions inside `onUnmounted` or `watchEffect` cleanup

---

## Pinia Store Skill

### Store template (always use Setup Store style)

```ts
// stores/useExampleStore.ts
import { defineStore } from 'pinia'

interface ExampleState {
  items: Item[]
  selectedId: string | null
  filter: string
}

export const useExampleStore = defineStore('example', () => {
  // --- State ---
  const items = ref<Item[]>([])
  const selectedId = ref<string | null>(null)
  const filter = ref('')

  // --- Getters (computed) ---
  const selectedItem = computed(() =>
    items.value.find(i => i.id === selectedId.value) ?? null,
  )

  const filteredItems = computed(() =>
    filter.value
      ? items.value.filter(i =>
          i.name.toLowerCase().includes(filter.value.toLowerCase()),
        )
      : items.value,
  )

  // --- Actions ---
  async function loadItems() {
    try {
      items.value = await $fetch<Item[]>('/api/items')
    } catch (err) {
      console.error('[ExampleStore] loadItems failed:', err)
    }
  }

  async function createItem(payload: CreateItemPayload) {
    const newItem = await $fetch<Item>('/api/items', {
      method: 'POST',
      body: payload,
    })
    items.value.push(newItem)
    return newItem
  }

  function selectItem(id: string) {
    selectedId.value = id
  }

  function setFilter(value: string) {
    filter.value = value
  }

  // --- Reset ---
  function $reset() {
    items.value = []
    selectedId.value = null
    filter.value = ''
  }

  return {
    // State (readonly to enforce actions)
    items: readonly(items),
    selectedId: readonly(selectedId),
    filter: readonly(filter),
    // Getters
    selectedItem,
    filteredItems,
    // Actions
    loadItems,
    createItem,
    selectItem,
    setFilter,
    $reset,
  }
})
```

### Using the store in a component

```ts
// In component
const store = useExampleStore()

// ✅ Use storeToRefs for reactive state
const { items, selectedItem, filteredItems } = storeToRefs(store)

// ✅ Destructure actions directly (not reactive, they're functions)
const { loadItems, createItem, selectItem } = store

onMounted(() => loadItems())
```

---

## State Management Decision Guide

This is one of the most common points of confusion in modern Vue development. The short answer: **Pinia and TanStack Query are not competitors — they solve different problems and are often used together in the same project.**

### The Core Mental Model: Who Owns the Data?

```
Is this data from a server / API / database?
  └─ YES → TanStack Query owns it
      Examples: user profiles, product lists, orders, search results

Is this data purely inside your app (no server round-trip needed)?
  └─ YES → Pinia owns it
      Examples: sidebar open/closed, dark mode, current step in a wizard, auth token
```

### When to Use TanStack Query

Use TanStack Query for any **server state** — data that lives on an external server and needs to be synchronized with your UI.

**What it handles automatically so you don't have to:**
- `isLoading`, `isError`, `data` states — no manual `ref` boilerplate
- **Caching** — navigate away and back, data is served from cache instantly
- **Deduplication** — two components requesting the same data = one network request
- **Background refetching** — stale data is refreshed when the tab regains focus
- **Pagination & infinite scroll** — built-in primitives
- **Memory management** — garbage-collects data no longer used by any component

```ts
// ❌ Old way — raw Axios in onMounted (every dev writes this boilerplate)
const users = ref<User[]>([])
const isLoading = ref(false)
const error = ref<Error | null>(null)

onMounted(async () => {
  isLoading.value = true
  try {
    const res = await axios.get('/api/users')
    users.value = res.data
  } catch (err) {
    error.value = err as Error
  } finally {
    isLoading.value = false
  }
})
// 👆 No caching. Refetches on every mount. Duplicates if two components need same data.

// ✅ TanStack Query way — all of the above, plus caching + dedup + background sync
import { useQuery } from '@tanstack/vue-query'

const { data: users, isLoading, isError, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => axios.get<User[]>('/api/users').then(res => res.data),
  staleTime: 1000 * 60 * 5, // treat as fresh for 5 minutes
})
```

### When to Use Pinia

Use Pinia for **client state** — data that exists purely inside your frontend and doesn't need a server round-trip.

| Good Pinia use cases | Why not TanStack Query |
|---|---|
| Dark mode / theme preference | No server — it's a UI toggle |
| Sidebar collapsed / expanded | Local UI state, no async |
| Current authenticated user's display info | Populated once from auth, then referenced everywhere |
| Multi-step form data across route changes | Temporary local data, not persisted server-side |
| Selected filters that affect multiple views | Shared UI state between components |
| Shopping cart (before checkout) | Local until user submits |

```ts
// stores/useUIStore.ts — perfect Pinia use case
export const useUIStore = defineStore('ui', () => {
  const isDarkMode = ref(false)
  const isSidebarOpen = ref(true)
  const activeLocale = ref<'en' | 'hi' | 'ur'>('en')

  function toggleDarkMode() { isDarkMode.value = !isDarkMode.value }
  function toggleSidebar() { isSidebarOpen.value = !isSidebarOpen.value }
  function setLocale(locale: typeof activeLocale.value) { activeLocale.value = locale }

  return { isDarkMode, isSidebarOpen, activeLocale, toggleDarkMode, toggleSidebar, setLocale }
})
```

### Axios vs TanStack Query — They're Not Alternatives

This is a key misconception. Think of them as different layers:

```
┌─────────────────────────────────────────┐
│         TanStack Query                  │  ← "WHEN": caching, retries, loading state,
│   manages the lifecycle of data         │     deduplication, background sync
├─────────────────────────────────────────┤
│              Axios                      │  ← "HOW": makes the actual HTTP call,
│   makes the actual HTTP request         │     handles headers, interceptors, auth tokens
└─────────────────────────────────────────┘
```

**The professional setup is to use Axios *inside* TanStack Query's `queryFn`:**

```ts
// lib/axios.ts — global Axios instance with auth interceptor
import axios from 'axios'

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
})

// Auth interceptor — attach token to every request automatically
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access-token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// Response interceptor — handle 401 globally
apiClient.interceptors.response.use(
  res => res,
  async (error) => {
    if (error.response?.status === 401) {
      await useAuthStore().logout()
    }
    return Promise.reject(error)
  },
)
```

```ts
// composables/useUsers.ts — TanStack Query + Axios together
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import { apiClient } from '@/lib/axios'

// Query (GET)
export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const { data } = await apiClient.get<User[]>('/users')
      return data
    },
    staleTime: 1000 * 60 * 2, // 2 minutes
  })
}

// Mutation (POST / PATCH / DELETE) + cache invalidation
export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (payload: CreateUserPayload) =>
      apiClient.post<User>('/users', payload).then(res => res.data),

    // After creating a user, invalidate the users list so it refetches
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}
```

```ts
// In a component — clean, no boilerplate
const { data: users, isLoading, isError } = useUsers()
const { mutate: createUser, isPending } = useCreateUser()

function handleSubmit(form: CreateUserPayload) {
  createUser(form)
}
```

### Decision Summary

| Scenario | Tool |
|---|---|
| Fetch a list from `/api/users` | TanStack Query |
| Fetch a single record by ID | TanStack Query |
| Paginated table / infinite scroll | TanStack Query |
| POST / PATCH / DELETE with cache update | TanStack Query `useMutation` |
| Dark mode toggle | Pinia |
| Auth user display info (navbar, avatar) | Pinia |
| Multi-step wizard form state | Pinia |
| Shared filters across multiple views | Pinia |
| Global HTTP headers / auth tokens | Axios interceptors |
| Simple one-off script / utility | Raw `fetch` or Axios directly |

### ⚠️ Key Rule: Don't Bridge the Two

Never copy data from TanStack Query into a Pinia store. This creates two sources of truth that will get out of sync.

```ts
// ❌ Anti-pattern — bridging TanStack Query into Pinia
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
watch(users, (val) => { userStore.setUsers(val) }) // Now you have two copies

// ✅ Correct — components consume TanStack Query directly
const { data: users } = useUsers() // that's it
```

### Setup: Installing TanStack Query in Vue 3

```ts
// main.ts
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,     // 1 minute default freshness
      retry: 2,                  // retry failed requests twice
      refetchOnWindowFocus: true, // refresh when tab regains focus
    },
  },
})

app.use(VueQueryPlugin, { queryClient })
```

---

## Vue Router Skill

### Typed route definitions

```ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('@/pages/HomePage.vue'),
    },
    {
      path: '/users/:id',
      name: 'UserDetail',
      component: () => import('@/pages/UserDetailPage.vue'),
      // Route-level guard
      beforeEnter: (to) => {
        if (!to.params.id) return { name: 'Home' }
      },
    },
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: () => import('@/layouts/DashboardLayout.vue'),
      meta: { requiresAuth: true },
      children: [
        {
          path: '',
          name: 'DashboardHome',
          component: () => import('@/pages/dashboard/DashboardHome.vue'),
        },
      ],
    },
    {
      path: '/:pathMatch(.*)*',
      name: 'NotFound',
      component: () => import('@/pages/NotFoundPage.vue'),
    },
  ],
})

// Global auth guard
router.beforeEach((to) => {
  const authStore = useAuthStore()
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

export default router
```

### Navigating in components

```ts
const router = useRouter()
const route = useRoute()

// Typed param access
const userId = computed(() => route.params.id as string)

// Navigate with named route
await router.push({ name: 'UserDetail', params: { id: '123' } })

// Navigate with query
await router.push({ name: 'Search', query: { q: searchTerm.value } })

// Go back
router.back()
```

---

## Reactivity Patterns Cheat Sheet

| Pattern | When to use |
|---|---|
| `ref<T>(value)` | Primitives, arrays, objects you'll reassign |
| `reactive({})` | Objects you won't destructure or reassign |
| `computed(() => ...)` | Derived values — cached until deps change |
| `watch(source, cb)` | Side effects when reactive data changes |
| `watchEffect(cb)` | Side effects that auto-track dependencies |
| `shallowRef()` | Large objects — only top-level reactivity needed |
| `toRefs(reactive)` | Destructure reactive without losing reactivity |
| `toValue(maybeRef)` | Unwrap ref or raw value inside composables |
| `readonly(ref)` | Expose state from composable/store without allowing mutation |
| `markRaw(obj)` | Non-reactive objects (chart instances, maps, sockets) |

---

## Common Component Patterns

### Async component with loading state
```vue
<script setup lang="ts">
const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorMessage,
  delay: 200,
  timeout: 5000,
})
</script>
```

### v-model on custom component
```vue
<!-- Parent -->
<MyInput v-model="email" />

<!-- MyInput.vue -->
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>
<template>
  <input :value="model" @input="model = $event.target.value" />
</template>
```

### Provide / Inject (typed)
```ts
// In parent
const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
provide(ThemeKey, ref('light'))

// In child
const theme = inject(ThemeKey) // typed as Ref<'light' | 'dark'> | undefined
```

### Teleport for modals
```vue
<Teleport to="body">
  <div v-if="isModalOpen" class="modal-overlay">
    <div class="modal">
      <slot />
    </div>
  </div>
</Teleport>
```

---

## Performance Checklist

Before completing any component, verify:
- [ ] `v-for` always has a stable `:key` (not index)
- [ ] `v-if` and `v-for` are never on the same element
- [ ] Heavy computations are in `computed()`, not template expressions
- [ ] Large non-reactive objects are wrapped in `markRaw()`
- [ ] Heavy components use `defineAsyncComponent()`
- [ ] Long lists (100+ items) use virtual scrolling
- [ ] Event listeners registered in `onMounted` are removed in `onUnmounted`

来源:https://github.com/Ashutosh012/vue-nuxt-skills