Modul 11: Praxis
CRUD-App: Kontakte-Manager
Eine vollständige Kontaktverwaltung mit Vue, Nuxt und PrimeVue
Projektübersicht
In diesem Kapitel bauen wir eine komplette CRUD-Anwendung, die alles zusammenführt: useFetch für API-Calls, Pinia für lokalen State, PrimeVue für die UI-Komponenten und die Server-API als Backend.
Die App bietet:
- Kontaktliste mit Echtzeit-Suche
- Erstellen und Bearbeiten über Dialoge
- Löschen mit Bestätigungsdialog
- Responsive DataTable mit Sortierung
Pinia Store für Kontakte
Der Store verwaltet den lokalen State und kommuniziert mit der API:
stores/kontakte.ts
export interface Kontakt {
id: number
name: string
email: string
telefon?: string
firma?: string
rolle?: string
}
export type NeuerKontakt = Omit<Kontakt, 'id'>
export const useKontakteStore = defineStore('kontakte', () => {
const kontakte = ref<Kontakt[]>([])
const suchbegriff = ref('')
const loading = ref(false)
const fehler = ref<string | null>(null)
// Gefilterte Kontakte
const gefilterteKontakte = computed(() => {
if (!suchbegriff.value) return kontakte.value
const suche = suchbegriff.value.toLowerCase()
return kontakte.value.filter(k =>
k.name.toLowerCase().includes(suche) ||
k.email.toLowerCase().includes(suche) ||
k.firma?.toLowerCase().includes(suche) ||
k.rolle?.toLowerCase().includes(suche)
)
})
const anzahl = computed(() => kontakte.value.length)
// Kontakte vom Server laden
async function laden() {
loading.value = true
fehler.value = null
try {
const { data } = await useFetch<Kontakt[]>('/api/kontakte')
if (data.value) kontakte.value = data.value
} catch (e: any) {
fehler.value = 'Kontakte konnten nicht geladen werden'
} finally {
loading.value = false
}
}
// Kontakt erstellen
async function erstellen(kontakt: NeuerKontakt) {
const neu = await \$fetch<Kontakt>('/api/kontakte', {
method: 'POST',
body: kontakt
})
kontakte.value.push(neu)
return neu
}
// Kontakt aktualisieren
async function aktualisieren(id: number, daten: Partial<Kontakt>) {
const aktualisiert = await \$fetch<Kontakt>(\\\`
const codeExample2 = Kontaktliste mit DataTable
pages/kontakte/index.vue
Erstellen/Bearbeiten-Dialog
components/KontaktDialog.vue
<template>
<Dialog
:visible="visible"
@update:visible="\$emit('update:visible', \$event)"
:header="modus === 'neu' ? 'Neuer Kontakt' : 'Kontakt bearbeiten'"
:modal="true"
:style="{ width: '500px' }"
>
<form @submit.prevent="speichern" class="formular">
<div class="feld">
<label for="name">Name *</label>
<InputText
id="name"
v-model="formular.name"
:class="{ 'p-invalid': fehler.name }"
placeholder="Vor- und Nachname"
/>
<small v-if="fehler.name" class="p-error">{{ fehler.name }}</small>
</div>
<div class="feld">
<label for="email">E-Mail *</label>
<InputText
id="email"
v-model="formular.email"
type="email"
:class="{ 'p-invalid': fehler.email }"
placeholder="name@firma.de"
/>
<small v-if="fehler.email" class="p-error">{{ fehler.email }}</small>
</div>
<div class="feld">
<label for="telefon">Telefon</label>
<InputText id="telefon" v-model="formular.telefon" placeholder="+49 ..." />
</div>
<div class="feld">
<label for="firma">Firma</label>
<InputText id="firma" v-model="formular.firma" placeholder="Firmenname" />
</div>
<div class="feld">
<label for="rolle">Rolle</label>
<InputText id="rolle" v-model="formular.rolle" placeholder="z.B. Entwickler" />
</div>
</form>
<template #footer>
<Button
label="Abbrechen"
severity="secondary"
@click="\$emit('update:visible', false)"
/>
<Button
:label="modus === 'neu' ? 'Erstellen' : 'Speichern'"
icon="pi pi-check"
@click="speichern"
:loading="speichert"
/>
</template>
</Dialog>
</template>
<script setup lang="ts">
import type { Kontakt, NeuerKontakt } from '~/stores/kontakte'
const props = defineProps<{
visible: boolean
kontakt: Kontakt | null
modus: 'neu' | 'bearbeiten'
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
'gespeichert': []
}>()
const store = useKontakteStore()
const speichert = ref(false)
const formular = ref<NeuerKontakt>({
name: '', email: '', telefon: '', firma: '', rolle: ''
})
const fehler = ref<Record<string, string>>({})
// Formular bei Öffnung befüllen
watch(() => props.visible, (sichtbar) => {
if (sichtbar && props.kontakt && props.modus === 'bearbeiten') {
formular.value = { ...props.kontakt }
} else if (sichtbar) {
formular.value = { name: '', email: '', telefon: '', firma: '', rolle: '' }
}
fehler.value = {}
})
function validieren(): boolean {
fehler.value = {}
if (!formular.value.name.trim()) fehler.value.name = 'Name ist erforderlich'
if (!formular.value.email.trim()) fehler.value.email = 'E-Mail ist erforderlich'
else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(formular.value.email)) {
fehler.value.email = 'Ungültiges E-Mail-Format'
}
return Object.keys(fehler.value).length === 0
}
async function speichern() {
if (!validieren()) return
speichert.value = true
try {
if (props.modus === 'neu') {
await store.erstellen(formular.value)
} else if (props.kontakt) {
await store.aktualisieren(props.kontakt.id, formular.value)
}
emit('gespeichert')
} catch (e: any) {
fehler.value.allgemein = e.data?.message || 'Speichern fehlgeschlagen'
} finally {
speichert.value = false
}
}
</script>Live-Demo
▶Kontakte-Manager
Interaktiv Die vollständige Demo zeigt eine interaktive Kontaktliste mit Suche, Erstellen, Bearbeiten und Löschen. Starte die App lokal mit npm run dev und öffne /kontakte.
| Name | Firma | Rolle | |
|---|---|---|---|
| Anna Schmidt | anna@beispiel.de | TechStart GmbH | Entwicklerin |
| Max Weber | max@beispiel.de | WebDesign AG | Designer |
| Lisa Müller | lisa@beispiel.de | DataFlow GmbH | Projektmanagerin |
Architektur-Überblick
Datenfluss der CRUD-App
┌─────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ KontaktDialog│───▶│ useKontakteStore │ │
│ │ (Formular) │ │ (Pinia) │ │
│ └──────────────┘ │ │ │
│ │ • kontakte[] │ │
│ ┌──────────────┐ │ • suchbegriff │ │
│ │ DataTable │◀──│ • loading │ │
│ │ (Liste) │ │ • gefilterteK. │ │
│ └──────────────┘ └────────┬────────────┘ │
│ │ \$fetch / useFetch │
└───────────────────────────────┼─────────────────────┘
│
┌───────────────────────────────┼─────────────────────┐
│ Nuxt Server (Nitro) │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ server/api/kontakte/ │ │
│ │ • index.get.ts → Liste + Suche │ │
│ │ • index.post.ts → Erstellen │ │
│ │ • [id].put.ts → Aktualisieren │ │
│ │ • [id].delete.ts → Löschen │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────▼────────────────────────┐ │
│ │ server/data/kontakte.json │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘ℹ️
Warum Pinia + useFetch?
useFetch holt die Daten vom Server. Pinia hält den lokalen State und ermöglicht optimistic Updates — die UI reagiert sofort, ohne auf die Server-Antwort zu warten. Bei einem Fehler wird zurückgerollt.
Zusammenfassung
💡
CRUD-App Bausteine
- Pinia Store — Zentraler State mit API-Kommunikation
- DataTable — PrimeVue-Tabelle mit Sortierung + Paginierung
- Dialog — Modale Formulare für Erstellen und Bearbeiten
- Validierung — Client-seitig im Formular, serverseitig in der API
- Suche — Computed Property filtert in Echtzeit
- Bestätigung — Lösch-Dialog verhindert Versehen
- useFetch + $fetch — SSR-sicher laden, client-seitig mutieren