Vue 3 Pinia Nuxt 3 TypeScript Claude Code

Vue 3 & Pinia mit Claude Code: State Management 2026

Vue 3 Composition API mit Pinia — defineStore, storeToRefs, Actions, Getters, Persistenz und Nuxt 3 Integration. Claude Code als dein Vue-Experte.

6. Mai 2026 10 min Lesezeit Deutsch
← Zurück zum Blog

Vue 3 hat das Frontend-Ökosystem 2026 fest im Griff. Mit der Composition API, vollständiger TypeScript-Unterstützung und Pinia als offiziellem State-Manager ist Vue 3 zur ersten Wahl für skalierbare Single-Page-Apps und Nuxt-3-Projekte geworden. In diesem Artikel zeigen wir, wie Claude Code als KI-gestützter Vue-Experte das komplette Setup — von defineStore bis zur serverseitigen Persistenz — in Minuten aufbaut.

Inhaltsverzeichnis

  1. Vue 3 Composition API
  2. Composables: Logik kapseln und wiederverwenden
  3. Pinia: defineStore im Detail
  4. storeToRefs & Actions
  5. Pinia Persistenz
  6. Nuxt 3 Integration

1. Vue 3 Composition API

Die Composition API ist das Herzstück von Vue 3. Sie löst die Options-API ab und ermöglicht es, Logik nach Funktion statt nach Typ zu gruppieren. Das Ergebnis: bessere Lesbarkeit, einfaches Testing und vollständige TypeScript-Inferenz.

Vue 3

Kernkonzepte im Überblick

ref und reactive im Vergleich

ref eignet sich für einzelne Werte und braucht im Template kein .value. reactive ist für Objekte gedacht, erlaubt aber kein Destructuring ohne Reaktivitätsverlust.

src/components/CounterDemo.vue
<!-- setup sugar syntax (empfohlen) --> <script setup lang="ts"> import { ref, reactive, computed, watch, watchEffect } from 'vue' // ref: primitiver Wert const count = ref<number>(0) const username = ref<string>('') // reactive: Objekt const user = reactive({ name: '', email: '', role: 'viewer' as 'viewer' | 'editor' | 'admin' }) // computed: gecacht bis abhängige refs ändern const doubleCount = computed(() => count.value * 2) const isAdmin = computed(() => user.role === 'admin') // watch: gezielt auf bestimmte Source reagieren watch(count, (newVal, oldVal) => { console.log(`count: ${oldVal} → ${newVal}`) }) // watchEffect: läuft sofort + bei jeder Dependency-Änderung watchEffect(() => { document.title = `Zähler: ${count.value}` }) function increment() { count.value++ } function reset() { count.value = 0 } </script> <template> <div> <p>Count: {{ count }} | Double: {{ doubleCount }}</p> <button @click="increment">+1</button> <button @click="reset">Reset</button> </div> </template>

TypeScript-Typen in Vue 3

Vue 3 nutzt TypeScript nativ. defineProps und defineEmits werden direkt mit Typen annotiert — kein separates PropType-Cast mehr nötig.

src/components/UserCard.vue
<script setup lang="ts"> // Props mit TypeScript-Interface interface Props { userId: number username: string role?: 'viewer' | 'editor' | 'admin' verified?: boolean } const props = withDefaults(defineProps<Props>(), { role: 'viewer', verified: false }) // Emits mit Typen const emit = defineEmits<{ (e: 'update:role', role: Props['role']): void (e: 'delete', userId: number): void }>() function promoteToAdmin() { emit('update:role', 'admin') } </script> <template> <div class="user-card"> <h3>{{ props.username }}</h3> <span>{{ props.role }}</span> <button @click="promoteToAdmin">Admin machen</button> <button @click="emit('delete', props.userId)">Löschen</button> </div> </template>

Claude Code Tipp: Schreib // @claude: generiere TypeScript Interface für User aus dieser JSON-Struktur in dein Vue-File — Claude Code ergänzt vollständige Typdefinitionen inkl. optionaler Felder.

Lifecycle Hooks in setup()

Alle Lifecycle Hooks stehen als importierbare Funktionen zur Verfügung. Sie können mehrfach registriert und modular in Composables verteilt werden.

import { onMounted, onUnmounted, onBeforeMount, onUpdated, onBeforeUpdate, onActivated, onDeactivated } from 'vue' const cleanup: (() => void)[] = [] onMounted(() => { const handler = () => console.log('resize') window.addEventListener('resize', handler) cleanup.push(() => window.removeEventListener('resize', handler)) }) onUnmounted(() => { cleanup.forEach(fn => fn()) }) onUpdated(() => { console.log('Komponente aktualisiert') })

2. Composables: Logik kapseln und wiederverwenden

Composables sind Funktionen, die die Composition API nutzen, um wiederverwendbare Logik zu kapseln. Sie ersetzen Mixins vollständig und ermöglichen Tree-Shaking, vollständige Typsicherheit und einfaches Testen.

Composable

Naming-Konvention: use* Präfix

Composables beginnen per Konvention mit use: useCounter, useFetch, useLocalStorage, useDebounce. Der Präfix signalisiert, dass reactive State zurückgegeben wird.

useCounter — Einfaches Composable

src/composables/useCounter.ts
import { ref, computed, Ref } from 'vue' interface UseCounterOptions { min?: number max?: number step?: number } interface UseCounterReturn { count: Ref<number> isMin: Ref<boolean> isMax: Ref<boolean> increment: () => void decrement: () => void reset: () => void } export function useCounter( initialValue = 0, options: UseCounterOptions = {} ): UseCounterReturn { const { min = -Infinity, max = Infinity, step = 1 } = options const count = ref(initialValue) const isMin = computed(() => count.value <= min) const isMax = computed(() => count.value >= max) function increment() { if (count.value + step <= max) count.value += step } function decrement() { if (count.value - step >= min) count.value -= step } function reset() { count.value = initialValue } return { count, isMin, isMax, increment, decrement, reset } }

useFetch — Daten asynchron laden

src/composables/useFetch.ts
import { ref, shallowRef, MaybeRef, toValue } from 'vue' import { watchEffect } from 'vue' export function useFetch<T>(url: MaybeRef<string>) { const data = shallowRef<T | null>(null) const error = ref<Error | null>(null) const loading = ref(false) watchEffect(async () => { data.value = null error.value = null loading.value = true try { const resolvedUrl = toValue(url) const res = await fetch(resolvedUrl) if (!res.ok) throw new Error(`HTTP ${res.status}`) data.value = await res.json() as T } catch (e) { error.value = e instanceof Error ? e : new Error('Unbekannter Fehler') } finally { loading.value = false } }) return { data, error, loading } } // Verwendung: // const { data, loading, error } = useFetch<User[]>('/api/users')

useLocalStorage — Reaktiver Browser-Speicher

src/composables/useLocalStorage.ts
import { ref, watch } from 'vue' export function useLocalStorage<T>(key: string, defaultValue: T) { const stored = localStorage.getItem(key) const initial: T = stored ? JSON.parse(stored) as T : defaultValue const value = ref<T>(initial) as Ref<T> watch(value, (newVal) => { localStorage.setItem(key, JSON.stringify(newVal)) }, { deep: true }) return value } // Verwendung: // const theme = useLocalStorage<'light' | 'dark'>('theme', 'light')

provide / inject — Dependency Injection

// Parent.vue import { provide } from 'vue' import type { InjectionKey } from 'vue' export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme') const theme = ref<'light' | 'dark'>('dark') provide(ThemeKey, theme) // Child.vue (beliebig tief verschachtelt) import { inject } from 'vue' import { ThemeKey } from '../Parent.vue' const theme = inject(ThemeKey) // Typ: Ref<'light' | 'dark'> | undefined

3. Pinia: defineStore im Detail

Pinia ist der offizielle State-Manager für Vue 3 und löst Vuex ab. Es bietet vollständige TypeScript-Inferenz ohne Boilerplate, modulare Stores und native DevTools-Unterstützung.

Kein Mutations-Overhead

State direkt mutieren — kein Vuex-Mutations-Boilerplate mehr.

TypeScript-First

Vollständige Typ-Inferenz für State, Getters und Actions ohne extra Config.

DevTools-Integration

Vue DevTools zeigt Store-State, Time-Travel und Action-History.

Modular & Tree-Shakeable

Jeder Store ist ein eigenständiges Modul. Ungenutzte Stores landen nicht im Bundle.

Pinia

Installation

npm install pinia # Optional: Persistenz-Plugin npm install pinia-plugin-persistedstate
// main.ts import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) app.use(createPinia()) app.mount('#app')

Options Store vs. Setup Store

Pinia bietet zwei Syntaxvarianten. Der Setup Store (empfohlen) nutzt direkt die Composition API:

Aspekt Options Store Setup Store (empfohlen)
Syntax { state, getters, actions } Composition API: ref, computed
TypeScript Explizite Typen nötig Vollständige Inferenz
Composables Eingeschränkt nutzbar Direkt verwendbar
Lernkurve Flach (Vuex-ähnlich) Etwas steiler
Empfehlung 2026 Legacy / Migration Neue Projekte

Options Store — Klassische Variante

src/stores/userStoreOptions.ts
import { defineStore } from 'pinia' interface User { id: number name: string email: string role: 'viewer' | 'editor' | 'admin' } interface UserState { currentUser: User | null users: User[] loading: boolean } export const useUserStoreOptions = defineStore('users-options', { state: (): UserState => ({ currentUser: null, users: [], loading: false }), getters: { isLoggedIn: (state) => state.currentUser !== null, adminUsers: (state) => state.users.filter(u => u.role === 'admin'), userCount: (state) => state.users.length }, actions: { async fetchUsers() { this.loading = true try { const res = await fetch('/api/users') this.users = await res.json() } finally { this.loading = false } } } })

Setup Store — Empfohlene Variante

src/stores/userStore.ts
import { defineStore } from 'pinia' import { ref, computed } from 'vue' interface User { id: number name: string email: string role: 'viewer' | 'editor' | 'admin' createdAt: string } export const useUserStore = defineStore('users', () => { // State: refs const currentUser = ref<User | null>(null) const users = ref<User[]>([]) const loading = ref(false) const error = ref<string | null>(null) // Getters: computed const isLoggedIn = computed(() => currentUser.value !== null) const adminUsers = computed(() => users.value.filter(u => u.role === 'admin')) const userCount = computed(() => users.value.length) // Actions: async functions async function fetchUsers() { loading.value = true error.value = null try { const res = await fetch('/api/users') if (!res.ok) throw new Error(`HTTP ${res.status}`) users.value = await res.json() as User[] } catch (e) { error.value = e instanceof Error ? e.message : 'Fehler beim Laden' } finally { loading.value = false } } async function login(email: string, password: string) { loading.value = true try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }) currentUser.value = await res.json() as User } finally { loading.value = false } } function logout() { currentUser.value = null } // Exportiere alles return { currentUser, users, loading, error, isLoggedIn, adminUsers, userCount, fetchUsers, login, logout } })

4. storeToRefs & Actions

Der häufigste Anfängerfehler mit Pinia: Store destrukturieren und dabei die Reaktivität verlieren. storeToRefs löst dieses Problem elegant.

Achtung: Niemals Store-State direkt destrukturieren! const { users } = useUserStore() bricht die Reaktivität — users ist dann ein normales Array, keine reactive Ref.

storeToRefs korrekt verwenden

src/components/UserList.vue
<script setup lang="ts"> import { storeToRefs } from 'pinia' import { useUserStore } from '../stores/userStore' const userStore = useUserStore() // RICHTIG: storeToRefs für State + Getters const { users, loading, error, isLoggedIn, adminUsers } = storeToRefs(userStore) // Actions direkt vom Store (keine storeToRefs nötig) const { fetchUsers, login, logout } = userStore // Wird korrekt reaktiv aufgerufen fetchUsers() </script> <template> <div> <div v-if="loading">Lädt...</div> <div v-else-if="error">{{ error }}</div> <ul v-else> <li v-for="user in users" :key="user.id"> {{ user.name }} — {{ user.role }} </li> </ul> <p>Admins: {{ adminUsers.length }}</p> </div> </template>

$patch — Gebündelter State-Update

Mit $patch können mehrere State-Felder atomisch aktualisiert werden. Als Objekt oder als Callback-Funktion.

const store = useUserStore() // Objekt-Variante: nur geänderte Felder angeben store.$patch({ loading: false, error: null }) // Callback-Variante: für komplexe Mutationen (z.B. Arrays) store.$patch((state) => { state.users.push({ id: 99, name: 'Test', email: 't@t.de', role: 'viewer', createdAt: '' }) state.loading = false })

$reset — State zurücksetzen

// Nur bei Options Store direkt verfügbar store.$reset() // Setup Store: $reset manuell implementieren export const useCartStore = defineStore('cart', () => { const items = ref<CartItem[]>([]) const total = computed(() => items.value.reduce((s, i) => s + i.price, 0)) function $reset() { items.value = [] } return { items, total, $reset } })

$subscribe — State-Änderungen beobachten

const cartStore = useCartStore() // Subscription: läuft bei jedem State-Change cartStore.$subscribe((mutation, state) => { console.log('Cart geändert:', mutation.type) // 'direct' | 'patch object' | 'patch function' console.log('Neue Items:', state.items.length) // Beispiel: in Analytics senden analytics.track('cart_updated', { itemCount: state.items.length }) }, { detached: true // weiter laufen auch wenn Komponente unmounted }) // Action-Subscription cartStore.$onAction(({ name, args, after, onError }) => { console.log(`Action "${name}" gestartet`, args) after((result) => { console.log(`Action "${name}" beendet`, result) }) onError((error) => { console.error(`Action "${name}" fehlgeschlagen`, error) }) })

Claude Code Tipp: Öffne deinen Store in Claude Code und schreib // @claude: füge $onAction Logging für alle Actions hinzu — Claude Code implementiert vollständiges Action-Tracking inkl. Error-Handling und Performance-Messung.

5. Pinia Persistenz

pinia-plugin-persistedstate ist das Standard-Plugin für Store-Persistenz in Vue 3. Es serialisiert automatisch State in localStorage oder sessionStorage und hydratisiert ihn beim App-Start.

Plugin einrichten

src/main.ts
import { createApp } from 'vue' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import App from './App.vue' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) createApp(App) .use(pinia) .mount('#app')

Einfache Persistenz aktivieren

src/stores/settingsStore.ts
import { defineStore } from 'pinia' import { ref } from 'vue' export const useSettingsStore = defineStore('settings', () => { const theme = ref<'light' | 'dark'>('dark') const language = ref<'de' | 'en'>('de') const notifications = ref(true) return { theme, language, notifications } }, { persist: true // kompletter Store in localStorage })

Selektive Persistenz — nur bestimmte Felder

src/stores/authStore.ts
import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useAuthStore = defineStore('auth', () => { const accessToken = ref<string | null>(null) const refreshToken = ref<string | null>(null) const userId = ref<number | null>(null) const sessionData = ref<Record<string, unknown>>({}) // NICHT persistieren const isAuthenticated = computed(() => !!accessToken.value) async function refreshAuth() { if (!refreshToken.value) return const res = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Authorization': `Bearer ${refreshToken.value}` } }) const { access_token } = await res.json() accessToken.value = access_token } return { accessToken, refreshToken, userId, sessionData, isAuthenticated, refreshAuth } }, { persist: { key: 'auth-store', storage: localStorage, pick: ['accessToken', 'refreshToken', 'userId'] // sessionData wird NICHT persistiert } })

Custom Serializer — sichere Token-Verschlüsselung

import { CryptoES } from 'crypto-es' const SECRET = import.meta.env.VITE_STORAGE_SECRET export const useSecureStore = defineStore('secure', () => { const sensitiveData = ref<string[]>([]) return { sensitiveData } }, { persist: { serializer: { serialize: (value) => CryptoES.AES.encrypt(JSON.stringify(value), SECRET).toString(), deserialize: (value) => JSON.parse(CryptoES.AES.decrypt(value, SECRET).toString(CryptoES.enc.Utf8)) } } })
Pinia

SessionStorage für temporäre Daten

Für Warenkorb oder Wizard-States: sessionStorage löscht Daten beim Tab-Schließen.

persist: { storage: sessionStorage, pick: ['cartItems', 'wizardStep'] }

6. Nuxt 3 Integration

Nuxt 3 ist das Full-Stack-Framework auf Vue-3-Basis. Es bringt SSR, SSG, Server Routes und Auto-Imports out of the box. Pinia wird als offiziell unterstütztes State-Management integriert.

Nuxt 3

Pinia in Nuxt 3 einrichten

npx nuxi init my-app cd my-app npm install @pinia/nuxt pinia pinia-plugin-persistedstate
// nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], pinia: { storesDirs: ['./stores/**'] // auto-import Stores } })

useState — Nuxt's reaktiver Server-State

useState ist Nuxt's eingebautes Composable für SSR-sicheren State. State wird zwischen Server und Client automatisch synchronisiert (Hydration).

// composables/useAppState.ts export const useAppState = () => { const counter = useState<number>('counter', () => 0) const theme = useState<'light' | 'dark'>('theme', () => 'dark') function increment() { counter.value++ } function toggleTheme() { theme.value = theme.value === 'dark' ? 'light' : 'dark' } return { counter, theme, increment, toggleTheme } }

useFetch und useAsyncData — Datenfetching mit SSR

pages/users.vue
<script setup lang="ts"> interface User { id: number name: string email: string } // useFetch: automatisches SSR + Client-Caching const { data: users, pending, error, refresh } = await useFetch<User[]>( '/api/users', { key: 'users-list', transform: (data) => data.sort((a, b) => a.name.localeCompare(b.name)) } ) // useAsyncData: für komplexere Fetch-Logik const { data: stats } = await useAsyncData('user-stats', async () => { const [total, active] = await Promise.all([ $fetch<number>('/api/users/count'), $fetch<number>('/api/users/active-count') ]) return { total, active, inactive: total - active } }) </script> <template> <div> <div v-if="pending">Lädt Benutzer...</div> <div v-else-if="error">Fehler: {{ error.message }}</div> <ul v-else> <li v-for="user in users" :key="user.id">{{ user.name }}</li> </ul> <button @click="refresh()">Neu laden</button> <p v-if="stats">{{ stats.active }} / {{ stats.total }} aktiv</p> </div> </template>

Server Components in Nuxt 3

Mit dem .server.vue-Suffix werden Komponenten ausschließlich auf dem Server gerendert. Perfekt für datenintensive Bereiche ohne Client-JS-Overhead.

<!-- components/HeavyChart.server.vue --> <!-- Läuft NUR auf dem Server — kein JavaScript im Browser! --> <script setup lang="ts"> const { data: chartData } = await useFetch('/api/chart-data') </script> <template> <div class="chart"> <!-- statisches HTML, kein Hydrationsbedarf --> <svg v-for="point in chartData" :key="point.id">...</svg> </div> </template>

Pinia Store in Nuxt 3 mit Persistenz

stores/cart.ts
import { defineStore } from 'pinia' import { ref, computed } from 'vue' interface CartItem { id: number name: string price: number quantity: number } // In Nuxt 3: Auto-Import wenn in stores/ Verzeichnis export const useCartStore = defineStore('cart', () => { const items = ref<CartItem[]>([]) const itemCount = computed(() => items.value.reduce((acc, item) => acc + item.quantity, 0) ) const total = computed(() => items.value.reduce((acc, item) => acc + item.price * item.quantity, 0) ) function addItem(item: Omit<CartItem, 'quantity'>) { const existing = items.value.find(i => i.id === item.id) if (existing) { existing.quantity++ } else { items.value.push({ ...item, quantity: 1 }) } } function removeItem(id: number) { items.value = items.value.filter(i => i.id !== id) } function clearCart() { items.value = [] } return { items, itemCount, total, addItem, removeItem, clearCart } }, { persist: { storage: typeof window !== 'undefined' ? localStorage : null, pick: ['items'] // SSR-sicher: nur client-side persistieren } })

Claude Code + Nuxt: Mit // @claude: generiere Server Route für /api/users mit Supabase und TypeScript erstellt Claude Code vollständige Nuxt Server Routes inkl. Fehlerbehandlung, Zod-Validation und TypeScript-Typen — in unter einer Minute.

Claude Code als Vue-Entwicklungspartner

Claude Code versteht Vue 3, Pinia und Nuxt 3 auf Experten-Niveau. Typische Workflows, die in Minuten statt Stunden erledigt sind:

Vue 3 & Pinia mit KI beschleunigen

Teste Claude Code kostenlos und erlebe, wie schnell du Vue-3-Projekte mit KI-Unterstützung realisierst — von defineStore bis zur Nuxt-3-Deployment-Pipeline.

Kostenlos starten — kein Kreditkarte nötig