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.

NameE-MailFirmaRolle
Anna Schmidtanna@beispiel.deTechStart GmbHEntwicklerin
Max Webermax@beispiel.deWebDesign AGDesigner
Lisa Müllerlisa@beispiel.deDataFlow GmbHProjektmanagerin

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