Claude Code for Vue 3: Composition API, Pinia, and Nuxt 4 Patterns — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Vue 3: Composition API, Pinia, and Nuxt 4 Patterns
Frontend

Claude Code for Vue 3: Composition API, Pinia, and Nuxt 4 Patterns

Published: December 13, 2026
Read time: 8 min read
By: Claude Skills 360

Vue 3’s Composition API with <script setup> provides fine-grained reactivity, better TypeScript integration, and shareable composables. Pinia replaces Vuex with a simpler, TypeScript-first store design. Nuxt 4 adds file-based server-side rendering, server routes (/server/api), and auto-imports. Claude Code generates Vue components with the Composition API, Pinia stores, custom composables, and the Nuxt server routes that power full-stack Vue apps.

CLAUDE.md for Vue 3 Projects

## Vue 3 Stack
- Vue 3 + TypeScript with <script setup> syntax (no Options API)
- State: Pinia (not Vuex) — one store per domain
- Router: Vue Router 4 with typed routes
- Build: Vite 6 with @vitejs/plugin-vue
- Testing: Vitest + Vue Test Utils 2 (@vue/test-utils)
- Framework: Nuxt 4 for full-stack (or Vite-only SPA)
- CSS: Tailwind + scoped styles in single-file components
- Forms: VeeValidate v4 + Zod
- HTTP: ofetch (Nuxt built-in) or axios for SPA

Vue Component with script setup

<!-- components/OrderCard.vue — fully typed Composition API component -->
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useOrderStore } from '@/stores/orders'
import { formatCurrency, formatDate } from '@/utils/formatters'

interface Order {
  id: string
  customerName: string
  status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
  totalCents: number
  createdAt: string
}

const props = defineProps<{
  order: Order
  showActions?: boolean
}>()

const emit = defineEmits<{
  cancel: [orderId: string]
  view: [orderId: string]
}>()

const router = useRouter()
const orderStore = useOrderStore()

// Computed properties
const statusColor = computed(() => ({
  pending: 'bg-yellow-100 text-yellow-800',
  processing: 'bg-blue-100 text-blue-800',
  shipped: 'bg-purple-100 text-purple-800',
  delivered: 'bg-green-100 text-green-800',
  cancelled: 'bg-red-100 text-red-800',
}[props.order.status]))

const isCancellable = computed(() =>
  ['pending'].includes(props.order.status)
)

const formattedTotal = computed(() =>
  formatCurrency(props.order.totalCents)
)

// Methods
const handleView = () => {
  router.push({ name: 'order-detail', params: { id: props.order.id } })
  emit('view', props.order.id)
}

const handleCancel = async () => {
  await orderStore.cancelOrder(props.order.id)
  emit('cancel', props.order.id)
}
</script>

<template>
  <div class="border rounded-lg p-4 hover:shadow-md transition-shadow">
    <div class="flex justify-between items-start">
      <div>
        <h3 class="font-semibold text-gray-900">#{{ order.id.slice(-6) }}</h3>
        <p class="text-sm text-gray-600">{{ order.customerName }}</p>
        <p class="text-xs text-gray-400 mt-1">{{ formatDate(order.createdAt) }}</p>
      </div>
      
      <div class="text-right">
        <p class="font-semibold text-gray-900">{{ formattedTotal }}</p>
        <span :class="['text-xs px-2 py-1 rounded-full mt-1 inline-block', statusColor]">
          {{ order.status }}
        </span>
      </div>
    </div>
    
    <div v-if="showActions" class="mt-3 flex gap-2">
      <button
        @click="handleView"
        class="btn btn-secondary btn-sm"
      >
        View Details
      </button>
      <button
        v-if="isCancellable"
        @click="handleCancel"
        class="btn btn-danger btn-sm"
        :disabled="orderStore.cancellingIds.has(order.id)"
      >
        {{ orderStore.cancellingIds.has(order.id) ? 'Cancelling...' : 'Cancel' }}
      </button>
    </div>
  </div>
</template>

Pinia Store

// stores/orders.ts — Pinia store with TypeScript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Order, CreateOrderInput } from '@/types'

export const useOrderStore = defineStore('orders', () => {
  // State
  const orders = ref<Order[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)
  const cancellingIds = ref<Set<string>>(new Set())
  
  // Getters (computed)
  const pendingOrders = computed(() =>
    orders.value.filter(o => o.status === 'pending')
  )
  
  const orderById = computed(() =>
    (id: string) => orders.value.find(o => o.id === id)
  )
  
  const totalRevenue = computed(() =>
    orders.value
      .filter(o => o.status === 'delivered')
      .reduce((sum, o) => sum + o.totalCents, 0)
  )
  
  // Actions
  async function fetchOrders(params?: { status?: string; query?: string }) {
    isLoading.value = true
    error.value = null
    
    try {
      const data = await $fetch<Order[]>('/api/orders', { query: params })
      orders.value = data
    } catch (e: any) {
      error.value = e.message
    } finally {
      isLoading.value = false
    }
  }
  
  async function createOrder(input: CreateOrderInput): Promise<Order> {
    const order = await $fetch<Order>('/api/orders', {
      method: 'POST',
      body: input,
    })
    orders.value.unshift(order)
    return order
  }
  
  async function cancelOrder(id: string): Promise<void> {
    cancellingIds.value.add(id)
    
    try {
      const updated = await $fetch<Order>(`/api/orders/${id}/cancel`, {
        method: 'POST',
      })
      
      const index = orders.value.findIndex(o => o.id === id)
      if (index !== -1) {
        orders.value[index] = updated
      }
    } finally {
      cancellingIds.value.delete(id)
    }
  }
  
  return {
    // State
    orders,
    isLoading,
    error,
    cancellingIds,
    // Getters
    pendingOrders,
    orderById,
    totalRevenue,
    // Actions
    fetchOrders,
    createOrder,
    cancelOrder,
  }
})

Composable for Data Fetching

// composables/useOrders.ts — reusable fetch composable
import { ref, watch, toValue, type MaybeRefOrGetter } from 'vue'
import type { Order } from '@/types'

export function useOrders(initialQuery: MaybeRefOrGetter<string> = '') {
  const orders = ref<Order[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)
  
  const fetch = async (query: string) => {
    isLoading.value = true
    error.value = null
    
    try {
      const data = await $fetch<Order[]>('/api/orders', {
        query: { q: query },
      })
      orders.value = data
    } catch (e: any) {
      error.value = e.message
    } finally {
      isLoading.value = false
    }
  }
  
  // Watch for query changes and re-fetch
  watch(
    () => toValue(initialQuery),
    (q) => fetch(q),
    { immediate: true }
  )
  
  return { orders, isLoading, error, refetch: () => fetch(toValue(initialQuery)) }
}

// Usage in component:
// const searchQuery = ref('')
// const { orders, isLoading } = useOrders(searchQuery)
// Both `searchQuery` as a ref or a plain string work

Nuxt 4 Page + Server Route

<!-- pages/orders/index.vue — Nuxt 4 file-based routing -->
<script setup lang="ts">
import { ref, computed } from 'vue'

// Nuxt 4 composables — auto-imported
const { data: orders, refresh, pending } = await useFetch('/api/orders', {
  query: { page: 1 },
})

const searchQuery = ref('')

// useAsyncData for conditional fetch
const { data: searchResults } = await useAsyncData(
  'order-search',
  () => $fetch('/api/orders/search', { query: { q: searchQuery.value } }),
  { watch: [searchQuery] }
)

const displayedOrders = computed(() =>
  searchQuery.value ? (searchResults.value ?? []) : (orders.value ?? [])
)

useSeoMeta({
  title: 'Orders',
  description: 'Manage your orders',
})
</script>

<template>
  <div>
    <h1>Orders</h1>
    
    <input
      v-model="searchQuery"
      placeholder="Search orders..."
      class="search-input"
    />
    
    <div v-if="pending">Loading...</div>
    
    <div v-else-if="displayedOrders.length === 0">
      No orders found.
    </div>
    
    <div v-else class="grid gap-4">
      <OrderCard
        v-for="order in displayedOrders"
        :key="order.id"
        :order="order"
        :show-actions="true"
      />
    </div>
    
    <button @click="refresh">Refresh</button>
  </div>
</template>
// server/api/orders/index.get.ts — Nuxt server route
import { defineEventHandler, getQuery } from 'h3'
import { z } from 'zod'
import { db } from '~/server/lib/db'

const QuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  status: z.string().optional(),
})

export default defineEventHandler(async (event) => {
  const query = QuerySchema.parse(getQuery(event))
  
  // Check auth
  const user = event.context.user
  if (!user) throw createError({ statusCode: 401 })
  
  const orders = await db.orders.list({
    userId: user.id,
    ...query,
  })
  
  return orders
})

Component Testing with Vitest

// components/__tests__/OrderCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import OrderCard from '../OrderCard.vue'

const mockOrder = {
  id: 'ord_123',
  customerName: 'Alice Smith',
  status: 'pending' as const,
  totalCents: 4999,
  createdAt: '2026-12-13T10:00:00Z',
}

describe('OrderCard', () => {
  beforeEach(() => setActivePinia(createPinia()))
  
  it('renders order details', () => {
    const wrapper = mount(OrderCard, {
      props: { order: mockOrder },
    })
    
    expect(wrapper.text()).toContain('ord_123'.slice(-6))
    expect(wrapper.text()).toContain('Alice Smith')
    expect(wrapper.text()).toContain('$49.99')
  })
  
  it('shows cancel button for pending orders', () => {
    const wrapper = mount(OrderCard, {
      props: { order: mockOrder, showActions: true },
    })
    
    expect(wrapper.find('[data-testid="cancel-btn"]').exists()).toBe(true)
  })
  
  it('emits cancel event on click', async () => {
    const wrapper = mount(OrderCard, {
      props: { order: mockOrder, showActions: true },
    })
    
    await wrapper.find('[data-testid="cancel-btn"]').trigger('click')
    
    expect(wrapper.emitted('cancel')).toHaveLength(1)
    expect(wrapper.emitted('cancel')![0]).toEqual(['ord_123'])
  })
})

For the Nuxt 4 advanced patterns including middleware and plugins, see the Nuxt guide for full-stack Nuxt development. For the HTMX and Alpine.js alternative for simpler server-rendered interactivity, the HTMX guide covers the hypermedia approach. The Claude Skills 360 bundle includes Vue 3 skill sets covering Composition API, Pinia stores, and Nuxt server routes. Start with the free tier to try Vue component generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free