
CRUD im Frontend: React Hook Form & Zod mit API

Wenn du ein professionelles Headless CMS baust, ist die Architektur aus React Hook Form Zod der absolute Goldstandard für jegliche Art der Dateneingabe.
In Teil 7 unserer Masterclass haben wir das Routing perfektioniert und zwei isolierte Welten für Admin und Public geschaffen. Die Startseite für unsere Leser lädt dank ISR-Caching pfeilschnell, und die Routen sitzen. Doch ein CMS ist völlig wertlos, wenn Redakteure nicht komfortabel neue Inhalte erschaffen können. Wir müssen echte Daten durch die Leitung schießen!
Wir kehren in unser geschütztes (admin) Dashboard zurück und implementieren heute vollständige CRUD-Operationen (Create, Read, Update, Delete). Wer schon einmal versucht hat, ein komplexes Formular mit Dutzenden Eingabefeldern in nativem React zu bauen, kennt den Schmerz: Endlose useState-Ketten, ruckelnde Interfaces bei jedem Tastendruck und fehleranfällige Validierungslogik. Damit ist jetzt endgültig Schluss.
1. Die Revolution: Warum useState für Formulare tot ist
Traditionell haben React-Entwickler für jedes Eingabefeld einen eigenen State (const [title, setTitle] = useState('')) angelegt. Das fatale Problem dabei: Bei jedem einzelnen Tastendruck des Nutzers ändert sich der State und die komplette React-Komponente wird neu gerendert (Re-Render).
Bei einem winzigen Login-Formular mit zwei Feldern bemerkst du das kaum. Aber bei einem gigantischen CMS-Artikel-Editor mit Meta-Daten, Tags, dynamischen Kategorien und umfangreichen Textinhalten zwingt das den Browser des Nutzers gnadenlos in die Knie. Die Eingabe beginnt zu laggen.
Hier kommt das absolute Dreamteam der modernen Webentwicklung ins Spiel: Die Symbiose aus React Hook Form Zod.
React Hook Form nutzt sogenannte "Uncontrolled Components". Es registriert die HTML-Eingabefelder (Inputs), lässt den nativen Browser die Tipparbeit machen und greift die Daten erst in dem Moment ab, wenn sie wirklich benötigt werden (z. B. exakt beim Klick auf den "Speichern"-Button). Das Ergebnis? Zero Re-Renders während des Tippens. Absolute Höchstgeschwindigkeit.
Und was ist mit Zod? Zod ist dein eiserner Türsteher. Anstatt für jedes Feld mühsam if (title.length < 5) zu schreiben, definieren wir einmalig ein striktes Daten-Schema (einen Bauplan). Zod prüft das einkommende Formular in Millisekunden gegen diesen Bauplan und wirft vollautomatisch präzise, übersetzte Fehlermeldungen für die Benutzeroberfläche aus, noch bevor der HTTP-Request unser Next.js Frontend überhaupt verlässt. Das ist "Defense in Depth" (Tiefenverteidigung) auf Enterprise-Niveau, gepaart mit perfekter User Experience.
2. Das Fundament gießen: Vorbereitungen
Dank unseres durchdachten Setups mit shadcn/ui (aus Teil 5) haben wir einen Großteil der Arbeit bereits indirekt erledigt. Als wir damals die <Form /> Komponenten über die shadcn-CLI installiert haben, wurden die Basis-Pakete bereits heruntergeladen.
Zur Sicherheit prüfen wir das aber in unserem Frontend-Terminal (cms-frontend) und installieren die Resolver, falls sie noch fehlen:
npm install react-hook-form @hookform/resolvers zodDas Paket @hookform/resolvers ist das absolute Herzstück dieser Magie. Es bringt React Hook Form bei, wie es nahtlos mit unserem Zod-Schema kommunizieren kann. Es übersetzt quasi die strenge Sprache von Zod in die Fehlermeldungen, die React Hook Form im UI anzeigen kann.
Was genau bauen wir in diesem Kapitel?
Unser Ziel ist ein hochgradig interaktiver Artikel-Editor im Admin-Bereich, der sich wie eine native Applikation anfühlt. Der Redakteur gibt einen Titel ein, und das Formular validiert in Echtzeit, ob dieser lang genug ist. Wir bauen ein Auswahl-Feld (Select) für Kategorien, deren Daten wir live über Sanctum aus unserer Laravel-API abrufen. Wir integrieren Ladezustände (isSubmitting) und senden die validierten Datenpakete anschließend sicher über unsere Axios-Instanz an das Backend. Nach erfolgreichem Speichern leiten wir den Nutzer elegant zurück zur Datentabelle.

3. Der eiserne Türsteher: Das Zod-Schema definieren
Bevor wir auch nur ein einziges visuelles Eingabefeld (UI) bauen, müssen wir die Spielregeln festlegen. In einer modernen React Hook Form Zod Architektur ist das Schema die absolute "Single Source of Truth" (die einzige Wahrheitsquelle). Es definiert nicht nur haargenau, welche Daten erlaubt sind, sondern generiert auch völlig automatisch die perfekten TypeScript-Typen für unser Formular. Wir sparen uns also das fehleranfällige, doppelte Schreiben von Interfaces!
Lege im Ordner deines Admin-Bereichs eine neue Datei für das "Post erstellen"-Formular an: src/app/(admin)/admin/posts/create/page.tsx.
Füge zunächst diesen Code im oberen Bereich der Datei ein:
1'use client'; // Extrem wichtig: Interaktive Formulare MÜSSEN Client Components sein!
2
3import { useForm } from 'react-hook-form';
4import { zodResolver } from '@hookform/resolvers/zod';
5import * as z from 'zod';
6import { useState } from 'react';
7import { useRouter } from 'next/navigation';
8import axios from '@/lib/axios'; // Unsere Sanctum-Axios Instanz aus Teil 4
9
10// 1. Das strikte Zod-Schema definieren (Unser Türsteher)
11const postSchema = z.object({
12 title: z.string().min(5, 'Der Titel muss mindestens 5 Zeichen lang sein.').max(255),
13 slug: z.string()
14 .min(5, 'Der Slug ist zu kurz.')
15 .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Nur Kleinbuchstaben und Bindestriche erlaubt.'),
16 content: z.string().min(20, 'Der Inhalt muss mindestens 20 Zeichen umfassen.'),
17 category_id: z.coerce.number().min(1, 'Bitte wähle eine gültige Kategorie aus.'),
18 is_published: z.boolean().default(false),
19});
20
21// 2. TypeScript-Typen magisch aus dem Schema extrahieren!
22type PostFormValues = z.infer<typeof postSchema>;Das ist Code-Eleganz in Reinform. Schau dir die Eigenschaft category_id an. Native HTML-Select-Felder geben standardmäßig immer Strings (Text) zurück, selbst wenn der Nutzer die Kategorie-ID "3" auswählt. Unsere Laravel API erwartet für Fremdschlüssel aber zwingend einen Integer (Zahl). Anstatt diesen Wert später mühsam per Hand umzuwandeln, nutzen wir z.coerce.number(). Zod erzwingt hier eine unsichtbare Typumwandlung, bevor React Hook Form die Daten überhaupt an unsere Submit-Funktion weitergibt.
4. Die Formular-Hülle: useForm initialisieren
Jetzt bauen wir die eigentliche React Component auf und verheiraten unsere beiden Technologien. Das useForm Hook ist das Gehirn und die Steuerzentrale unserer Dateneingabe.
Erweitere deine Datei genau unterhalb des Schemas um diesen Code:
1export default function CreatePostPage() {
2 const router = useRouter();
3 const [isSubmitting, setIsSubmitting] = useState(false);
4
5 // 3. Die perfekte React Hook Form Zod Verknüpfung
6 const form = useForm<PostFormValues>({
7 resolver: zodResolver(postSchema),
8 defaultValues: {
9 title: '',
10 slug: '',
11 content: '',
12 category_id: 0,
13 is_published: false,
14 },
15 mode: 'onChange', // Validiert in Echtzeit, während der Nutzer tippt!
16 });
17
18 // 4. Der Submit-Handler, der mit Laravel spricht
19 async function onSubmit(data: PostFormValues) {
20 setIsSubmitting(true);
21 try {
22 // Axios nutzt hier völlig automatisch unser unsichtbares Sanctum HttpOnly-Cookie
23 await axios.post('/api/posts', data);
24
25 // Bei Erfolg: Zurück zur Übersicht leiten und den Next.js Cache hart leeren
26 router.push('/admin/posts');
27 router.refresh();
28
29 } catch (error: any) {
30 console.error('Fehler beim Speichern:', error);
31 // Hier könnten wir später Laravel-Validierungsfehler aus dem Backend ins UI injizieren
32 } finally {
33 setIsSubmitting(false);
34 }
35 }
36
37 return (
38 <div className="max-w-4xl mx-auto py-8">
39 <h1 className="text-3xl font-bold mb-8 text-zinc-100">Neuen Artikel verfassen</h1>
40
41 {/* Hier kommt im nächsten Schritt das shadcn/ui Formular hin */}
42 <div className="bg-zinc-950 border border-zinc-800 rounded-xl p-8">
43 <p className="text-zinc-500 text-center">Formular-UI lädt...</p>
44 </div>
45
46 </div>
47 );
48}Was passiert hier im Maschinenraum? Wir übergeben dem useForm Hook unseren zodResolver(postSchema). Ab exakt diesem Moment weiß das Formular: "Ich darf den Submit-Prozess unter keinen Umständen ausführen, solange Zod nicht sein absolutes, grünes Licht gibt." Zusätzlich haben wir mode: 'onChange' gesetzt. Das bedeutet, sobald der Redakteur auch nur anfängt, den Titel zu tippen, feuert Zod seine Validierung ab. Da wir hier "Uncontrolled Components" nutzen, passiert das blitzschnell und ohne die gesamte Seite bei jedem Tastendruck neu zu rendern (Zero Re-Renders). Die User Experience bleibt butterweich, egal wie riesig dein Formular wird.

5. Das visuelle Meisterwerk: shadcn/ui Komponenten andocken
Wir haben das Gehirn unseres Formulars gebaut, jetzt brauchen wir den perfekten Körper. Erinnerst du dich daran, wie du in der Vergangenheit händisch rote Rahmen um fehlerhafte Eingabefelder programmieren musstest? Wie du mühsam verschachtelte CSS-Klassen und <span>{error.message}</span> unter jedes einzelne Feld geschrieben hast? Das war fehleranfällige Steinzeit-Entwicklung.
Dank der perfekten Vorintegration von React Hook Form Zod in die shadcn/ui Bibliothek (die wir in Teil 5 installiert haben), passiert all diese Magie nun völlig unsichtbar im Hintergrund. Wir nutzen dafür spezielle Wrapper-Komponenten (<Form>, <FormField>, <FormItem>), die direkt mit unserem useForm-Hook kommunizieren.
Ersetze den Platzhalter <p>Formular-UI lädt...</p> in deiner Datei src/app/(admin)/admin/posts/create/page.tsx mit folgendem Code (und vergiss nicht, die entsprechenden shadcn-Komponenten oben in der Datei zu importieren!):
1import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
2import { Input } from '@/components/ui/input';
3import { Textarea } from '@/components/ui/textarea';
4import { Button } from '@/components/ui/button';
5
6// ... (Dein vorheriger Code mit Schema und useForm bleibt unverändert) ...
7
8<Form {...form}>
9 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
10
11 {/* Feld: Titel */}
12 <FormField
13 control={form.control}
14 name="title"
15 render={({ field }) => (
16 <FormItem>
17 <FormLabel className="text-zinc-300">Artikel-Titel</FormLabel>
18 <FormControl>
19 <Input
20 placeholder="Ein epischer Titel..."
21 className="bg-zinc-900 border-zinc-800 text-white focus:ring-emerald-500"
22 {...field}
23 />
24 </FormControl>
25 {/* Hier erscheint magisch der Zod-Fehler in Rot! */}
26 <FormMessage className="text-red-400" />
27 </FormItem>
28 )}
29 />
30
31 {/* Feld: Slug */}
32 <FormField
33 control={form.control}
34 name="slug"
35 render={({ field }) => (
36 <FormItem>
37 <FormLabel className="text-zinc-300">URL-Slug</FormLabel>
38 <FormControl>
39 <Input
40 placeholder="ein-epischer-titel"
41 className="bg-zinc-900 border-zinc-800 text-white"
42 {...field}
43 />
44 </FormControl>
45 <FormMessage className="text-red-400" />
46 </FormItem>
47 )}
48 />
49
50 {/* Feld: Inhalt */}
51 <FormField
52 control={form.control}
53 name="content"
54 render={({ field }) => (
55 <FormItem>
56 <FormLabel className="text-zinc-300">Inhalt (Markdown)</FormLabel>
57 <FormControl>
58 <Textarea
59 placeholder="Schreibe deinen Content hier..."
60 className="min-h-[300px] bg-zinc-900 border-zinc-800 text-white resize-y"
61 {...field}
62 />
63 </FormControl>
64 <FormMessage className="text-red-400" />
65 </FormItem>
66 )}
67 />
68
69 <Button
70 type="submit"
71 disabled={isSubmitting}
72 className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-6"
73 >
74 {isSubmitting ? 'Artikel wird gespeichert...' : 'Artikel erstellen'}
75 </Button>
76
77 </form>
78</Form>Die Anatomie der Perfektion
Was genau macht dieser Code so überragend? Schau dir das Konstrukt <FormField> an. Wir übergeben ihm control={form.control} und den Namen des Feldes aus unserem Zod-Schema (name="title").
Das kleine, unscheinbare {...field} innerhalb des <Input>-Tags ist das absolute Meisterstück. Es nimmt alle Eigenschaften, die React Hook Form benötigt (value, onChange, onBlur, ref), und injiziert sie völlig automatisch in das native HTML-Feld. Du musst keinen einzigen State manuell verwalten!
Und die <FormMessage />? Diese Komponente lauscht ununterbrochen auf unser Zod-Schema. Wenn der Redakteur bei der Eingabe des Titels nach 3 Zeichen aufhört zu tippen, feuert Zod sofort den Fehler "Der Titel muss mindestens 5 Zeichen lang sein". Die <FormMessage /> fängt diesen Fehler ab, rendert ihn in wunderschönem Rot und – und das ist das Geniale an shadcn/ui – das dazugehörige <Input>-Feld erhält automatisch einen roten Rahmen (aria-invalid="true"), um die Accessibility (Barrierefreiheit) zu gewährleisten.
Das ist Frontend-Engineering auf Enterprise-Niveau. Dein Formular ist jetzt interaktiv, blitzschnell und durch unser React Hook Form Zod Setup absolut kugelsicher gegen Fehleingaben geschützt.

6. Der Endgegner: Dynamische Dropdowns (Select-Felder) meistern
Textfelder sind einfach. Der wahre Endgegner in der Frontend-Entwicklung sind Dropdowns (Select-Felder), deren Optionen nicht hart in den Code geschrieben sind, sondern live aus einer Datenbank geladen werden müssen. Wenn Redakteure einen Artikel schreiben, müssen sie ihn einer Kategorie zuordnen. Diese Kategorien müssen wir über unsere sichere Sanctum-Verbindung aus dem Laravel-Backend fetchen und in unser React Hook Form Zod Setup injizieren.
Früher endete das oft in einem Chaos aus asynchronen Race-Conditions und Formularen, die sich nicht absenden ließen, weil das Dropdown den Wert als Text ("3") statt als Zahl (3) übergeben hat. Wir lösen das heute auf Enterprise-Art.
Zuerst müssen wir unserem Formular beibringen, die Kategorien beim Laden der Seite abzurufen. Füge diesen State und den useEffect Hook im oberen Teil deiner CreatePostPage Komponente (direkt unter den useForm Aufruf) hinzu:
1// State für unsere dynamischen Kategorien aus Laravel
2 const [categories, setCategories] = useState<{ id: number; name: string }[]>([]);
3
4 // Kategorien asynchron beim ersten Rendern laden
5 useEffect(() => {
6 async function fetchCategories() {
7 try {
8 const response = await axios.get('/api/categories');
9 setCategories(response.data.data); // Das Array aus unserer Laravel API Resource
10 } catch (error) {
11 console.error('Kategorien konnten nicht geladen werden:', error);
12 }
13 }
14 fetchCategories();
15 }, []);Das shadcn/ui Select-Feld integrieren
Jetzt binden wir das Select-Feld in unsere bestehende <form> (unterhalb des Content-Textareas) ein. Stelle sicher, dass du die Select-Komponenten vorher über die CLI installiert hast (npx shadcn-ui@latest add select) und sie in deiner Datei importiert sind.
Füge diesen Block in dein Formular ein:
1{/* Feld: Kategorie (Dynamisches Dropdown) */}
2 <FormField
3 control={form.control}
4 name="category_id"
5 render={({ field }) => (
6 <FormItem>
7 <FormLabel className="text-zinc-300">Kategorie</FormLabel>
8
9 <Select
10 // Magie: Wir verknüpfen das shadcn Dropdown mit dem React Hook Form State!
11 onValueChange={field.onChange}
12 defaultValue={field.value ? field.value.toString() : ""}
13 >
14 <FormControl>
15 <SelectTrigger className="bg-zinc-900 border-zinc-800 text-white focus:ring-emerald-500">
16 <SelectValue placeholder="Wähle eine passende Kategorie..." />
17 </SelectTrigger>
18 </FormControl>
19 <SelectContent className="bg-zinc-900 border-zinc-800 text-white">
20 {categories.map((category) => (
21 <SelectItem
22 key={category.id}
23 value={category.id.toString()}
24 className="hover:bg-zinc-800 focus:bg-zinc-800 cursor-pointer"
25 >
26 {category.name}
27 </SelectItem>
28 ))}
29 </SelectContent>
30 </Select>
31
32 <FormMessage className="text-red-400" />
33 </FormItem>
34 )}
35 />Die geniale Typ-Umwandlung im Hintergrund
Lass uns kurz innehalten und die Brillanz dieses Codes würdigen. Ein HTML-Dropdown (und auch die komplexe Radix-UI Komponente, auf der shadcn basiert) verlangt immer Strings (Text) als value. Wir übergeben also category.id.toString().
Wenn der Nutzer nun auf "Tech" klickt (was die ID 5 hat), speichert das Select-Feld den String "5". Unsere Laravel API wird den Request aber mit einem strengen Validierungsfehler (422) ablehnen, weil in der Datenbank ein integer erwartet wird.
Müssen wir also vor dem Absenden (onSubmit) mühsam parseInt(data.category_id) schreiben? Nein! Erinnerst du dich an unseren Zod-Türsteher aus Schritt 3? Wir haben dort definiert: category_id: z.coerce.number(). Sobald das Select-Feld den String "5" an das Formular übergibt, greift Zod ein, wandelt den String blitzschnell und völlig unsichtbar in eine echte Zahl 5 um und reicht diesen perfekten Datentyp an unsere Axios-Anfrage weiter. Das Zusammenspiel aus React Hook Form Zod nimmt dir diese komplette, fehleranfällige Formatierungsarbeit ab.

7. Die Königsdisziplin: Update und asynchrones Pre-filling
Wir können nun makellos neue Artikel erschaffen. Doch was passiert, wenn unser Redakteur einen Tippfehler im Titel bemerkt und den Artikel bearbeiten (Update) möchte?
Das Bearbeiten von Daten ist in der Frontend-Welt berüchtigt. Du musst eine neue Seite laden (z. B. /admin/posts/[id]/edit), die ID aus der URL extrahieren, den bestehenden Artikel aus dem Laravel-Backend fetchen und diese Daten dann fehlerfrei in dein React Hook Form Zod Setup injizieren.
Der häufigste Anfängerfehler? Entwickler versuchen, den asynchronen Fetch-Request direkt in die defaultValues des useForm Hooks zu packen. Das funktioniert in React nicht, da das Formular beim ersten Rendern der Seite sofort gezeichnet wird – lange bevor die Antwort aus der Datenbank eintrifft. Die Felder bleiben leer.
Die magische reset() Funktion
Die Lösung ist eine unscheinbare, aber extrem mächtige Funktion von React Hook Form namens reset(). Wir lassen das Formular zunächst mit leeren Werten laden (oder zeigen unseren schönen Skeleton-Loader aus Teil 7). Sobald der useEffect Hook die Daten von Laravel erhält, rufen wir form.reset(daten) auf.
Erstelle die Datei src/app/(admin)/admin/posts/[id]/edit/page.tsx. Die Struktur ist fast identisch zu unserer "Create"-Seite, aber die Logik im Kopf der Datei ändert sich dramatisch:
1'use client';
2
3import { useEffect, useState } from 'react';
4import { useRouter, useParams } from 'next/navigation';
5import { useForm } from 'react-hook-form';
6import { zodResolver } from '@hookform/resolvers/zod';
7import * as z from 'zod';
8import axios from '@/lib/axios';
9
10// 1. Unser bewährtes Schema (exakt wie beim Create!)
11const postSchema = z.object({
12 title: z.string().min(5).max(255),
13 slug: z.string().min(5),
14 content: z.string().min(20),
15 category_id: z.coerce.number().min(1),
16 is_published: z.boolean(),
17});
18
19type PostFormValues = z.infer<typeof postSchema>;
20
21export default function EditPostPage() {
22 const router = useRouter();
23 const params = useParams(); // Holt die ID aus der URL (/admin/posts/15/edit)
24 const [isSubmitting, setIsSubmitting] = useState(false);
25 const [isLoading, setIsLoading] = useState(true);
26
27 const form = useForm<PostFormValues>({
28 resolver: zodResolver(postSchema),
29 // Wir starten absichtlich mit leeren Fallback-Werten
30 defaultValues: { title: '', slug: '', content: '', category_id: 0, is_published: false },
31 });
32
33 // 2. Bestehenden Artikel laden und in das Formular injizieren
34 useEffect(() => {
35 async function fetchPost() {
36 try {
37 const response = await axios.get(`/api/posts/${params.id}`);
38 const post = response.data.data;
39
40 // BÄM! Hier überschreiben wir das leere Formular mit echten Daten
41 form.reset({
42 title: post.title,
43 slug: post.slug,
44 content: post.content,
45 category_id: post.category_id,
46 is_published: post.is_published,
47 });
48
49 } catch (error) {
50 console.error('Artikel nicht gefunden', error);
51 } finally {
52 setIsLoading(false);
53 }
54 }
55 fetchPost();
56 }, [params.id, form]);
57
58 // 3. Update-Request (PUT statt POST)
59 async function onSubmit(data: PostFormValues) {
60 setIsSubmitting(true);
61 try {
62 // Wichtig: Für Updates nutzen wir in einer REST API die Methode PUT oder PATCH!
63 await axios.put(`/api/posts/${params.id}`, data);
64 router.push('/admin/posts');
65 router.refresh();
66 } finally {
67 setIsSubmitting(false);
68 }
69 }
70
71 if (isLoading) return <div className="text-zinc-400 p-8">Lade Artikel-Daten...</div>;
72
73 return (
74 // ... Hier folgt exakt das gleiche <Form> UI wie auf der Create-Seite!
75 <div>{/* UI Code */}</div>
76 );
77}Warum das Architektur in Perfektion ist
Schau dir den form.reset() Aufruf genau an. In dem Moment, in dem die Daten von Laravel eintreffen und wir reset() ausführen, passiert Magie: React Hook Form füllt nicht nur blitzschnell alle HTML-Inputs auf dem Bildschirm aus, sondern jagt diese Daten auch sofort durch unser Zod-Schema.
Sollte sich in der Zwischenzeit das Schema geändert haben (z. B. weil wir die Mindestlänge für Titel von 5 auf 10 Zeichen erhöht haben, der alte Artikel aber nur 6 Zeichen hat), leuchtet das Feld beim Laden sofort rot auf. Der Redakteur sieht sofort: "Oh, hier muss ich nachbessern, bevor ich speichern kann." Es gibt keine versteckten Fehler, keine bösen Überraschungen beim Absenden. Dein React Hook Form Zod System verhält sich wie ein lebendiger Organismus, der sich synchron mit deiner Laravel-API bewegt. Und da wir beim Speichern axios.put() nutzen, leitet Laravel den Request völlig automatisch an die update() Methode unseres Controllers (den wir in Teil 3 gebaut haben) weiter.
Das "U" in CRUD ist damit auf Enterprise-Niveau gemeistert!

8. Das "D" in CRUD: Sicheres Löschen mit Alert-Dialogen
Wir können nun Artikel erstellen (Create), in unserer pfeilschnellen Tabelle anzeigen (Read) und komplexe Updates mit unserem React Hook Form Zod Setup durchführen (Update). Fehlt nur noch ein Buchstabe zur absoluten Vollständigkeit: Das "D" für Delete (Löschen).
Ein naiver Entwickler klatscht für diese Aufgabe einfach einen roten Button in die Datentabelle und versieht ihn mit einem simplen onClick={() => axios.delete(id)}. Ein kurzes Zucken des Redakteurs an der Maus, ein falscher Klick, und der wichtigste Artikel des Monats ist für immer im digitalen Nirvana verschwunden. Eine absolute Katastrophe für die User Experience.
In einem Enterprise-System schützen wir zerstörerische Aktionen immer mit einem sogenannten Bestätigungs-Dialog (Confirmation Modal). Und anstatt uns nun mühsam mit z-index und dunklen Hintergrund-Overlays herumzuärgern, bedienen wir uns erneut an der Brillanz von shadcn/ui.
Installiere die alert-dialog Komponente in deinem Frontend:
npx shadcn-ui@latest add alert-dialogDie Delete-Button Komponente bauen
Da wir diesen Lösch-Button potenziell an vielen Stellen brauchen (in der Tabelle, auf der Detailseite), kapseln wir ihn in eine eigene kleine, smarte React Client Component. Erstelle die Datei src/components/admin/DeletePostButton.tsx:
1'use client';
2
3import { useState } from 'react';
4import { useRouter } from 'next/navigation';
5import axios from '@/lib/axios';
6import { Trash2 } from 'lucide-react'; // Ein schickes Icon
7
8import {
9 AlertDialog,
10 AlertDialogAction,
11 AlertDialogCancel,
12 AlertDialogContent,
13 AlertDialogDescription,
14 AlertDialogFooter,
15 AlertDialogHeader,
16 AlertDialogTitle,
17 AlertDialogTrigger,
18} from '@/components/ui/alert-dialog';
19import { Button } from '@/components/ui/button';
20
21interface DeletePostButtonProps {
22 postId: number;
23 postTitle: string;
24}
25
26export function DeletePostButton({ postId, postTitle }: DeletePostButtonProps) {
27 const router = useRouter();
28 const [isDeleting, setIsDeleting] = useState(false);
29
30 async function handleDelete() {
31 setIsDeleting(true);
32 try {
33 // 1. Der finale Schuss in Richtung Laravel Backend
34 await axios.delete(`/api/posts/${postId}`);
35
36 // 2. Next.js Cache hart leeren, damit die Tabelle sofort aktualisiert wird!
37 router.refresh();
38 } catch (error) {
39 console.error('Fehler beim Löschen', error);
40 } finally {
41 setIsDeleting(false);
42 }
43 }
44
45 return (
46 <AlertDialog>
47 {/* Der Trigger ist unser eigentlicher Button in der Tabelle */}
48 <AlertDialogTrigger asChild>
49 <Button variant="ghost" size="icon" className="text-red-400 hover:text-red-300 hover:bg-red-400/10">
50 <Trash2 className="h-4 w-4" />
51 </Button>
52 </AlertDialogTrigger>
53
54 {/* Das wunderschöne, abgedunkelte Modal */}
55 <AlertDialogContent className="bg-zinc-950 border border-zinc-800 text-zinc-100">
56 <AlertDialogHeader>
57 <AlertDialogTitle>Bist du dir absolut sicher?</AlertDialogTitle>
58 <AlertDialogDescription className="text-zinc-400">
59 Diese Aktion kann nicht rückgängig gemacht werden. Der Artikel
60 <span className="font-bold text-white"> "{postTitle}" </span>
61 wird dauerhaft aus der Datenbank gelöscht.
62 </AlertDialogDescription>
63 </AlertDialogHeader>
64
65 <AlertDialogFooter>
66 <AlertDialogCancel className="bg-transparent border-zinc-700 text-white hover:bg-zinc-800">
67 Abbrechen
68 </AlertDialogCancel>
69
70 <AlertDialogAction
71 onClick={(e) => {
72 e.preventDefault(); // Verhindert das sofortige Schließen, falls wir Lade-Spinner wollen
73 handleDelete();
74 }}
75 className="bg-red-600 hover:bg-red-700 text-white"
76 >
77 {isDeleting ? 'Wird gelöscht...' : 'Ja, endgültig löschen'}
78 </AlertDialogAction>
79 </AlertDialogFooter>
80 </AlertDialogContent>
81 </AlertDialog>
82 );
83}Die Perfekte Symbiose aus Client und Server
Du kannst diesen <DeletePostButton postId={post.id} postTitle={post.title} /> nun einfach in die Server Component deiner Datentabelle (aus Teil 5) einbauen.
Was passiert, wenn der Nutzer auf "Ja, endgültig löschen" klickt? Das Modal bleibt geöffnet und zeigt kurz "Wird gelöscht...". Axios sendet den Request (inklusive unsichtbarem Sanctum-Cookie) an Laravel. Laravel löscht den Datensatz und antwortet mit einem 200 OK.
Und dann kommt die Magie: router.refresh(). Diese Next.js-Funktion flüstert dem Server im Hintergrund zu: "Hey, die Daten haben sich geändert! Bitte rendere die aktuelle Tabelle serverseitig komplett neu und schicke mir nur das winzige Stück HTML, das sich verändert hat." Das Frontend wird nicht hart neu geladen (kein nerviger Whitescreen), aber die Tabellenzeile des gelöschten Artikels verschwindet wie von Zauberhand mit einem weichen Übergang. Das ist das ultimative Ziel einer perfekten Architektur.

Zusammenfassung: Der perfekte CRUD-Kreislauf
Wir haben den Kreis geschlossen. Mit der Implementierung dieses hochmodernen React Hook Form Zod Setups hast du die Zeiten von ruckelnden, fehleranfälligen und unübersichtlichen Formularen endgültig hinter dir gelassen. Dein Headless CMS besitzt nun eine interaktive, kugelsichere Dateneingabe auf absolutem Enterprise-Niveau.
Du hast gelernt, wie Zod als gnadenloser Türsteher fungiert, der einkommende Daten millisekundenschnell gegen einen strikten Bauplan validiert und sogar falsche Datentypen (z.coerce.number()) völlig unsichtbar korrigiert. Wir haben React Hook Form genutzt, um Re-Renders zu eliminieren und das Formular blitzschnell zu machen. Durch die nahtlose Integration mit den eleganten UI-Komponenten von shadcn haben wir ein visuelles Meisterwerk erschaffen, das dynamische API-Daten (Kategorien) asynchron lädt und bestehende Artikel für reibungslose Updates vorbefüllt (form.reset()). Und als krönenden Abschluss haben wir das gefürchtete Löschen (Delete) mit einem hochprofessionellen Bestätigungs-Dialog abgesichert, der über router.refresh() völlig weich mit dem serverseitigen Tabellen-Rendering synchronisiert.
Dein Admin-Dashboard ist jetzt eine mächtige, voll funktionsfähige Kommandozentrale.
Teil der Serie
Headless CMS mit Laravel und Next.js
Headless CMS mit Laravel 12 & Next.js: Der ultimative Guide Pillar
Das perfekte Laravel Next.js Setup: Projektstruktur für dein Headless CMS
Datenbank-Design für ein skalierbares Headless CMS
Perfekte RESTful API mit Laravel 12 & API Resources bauen
Sichere Laravel Sanctum Next.js Authentifizierung bauen
Das ultimative Next.js Admin Dashboard mit shadcn/ui bauen
Rollen & Rechte-Management mit Laravel Spatie integrieren
Next.js App Router Architektur für Admin & Public
CRUD im Frontend: React Hook Form & Zod mit API
Datei-Uploads und Media-Management über die API
React Server Components für maximalen SEO-Boost
Häufig gestellte Fragen (FAQ)
Ausblick auf Teil 9: Media-Management und Datei-Uploads
Texte und Kategorien sind das Rückgrat eines jeden CMS, aber was ist ein Blog ohne atemberaubende Bilder? Bisher feuern wir nur simple JSON-Texte über die Leitung. Wenn wir jedoch Bilder hochladen wollen, ändern sich die Spielregeln des Internets komplett.
Im nächsten Teil unserer Masterclass stellen wir uns dem komplexesten Thema der API-Entwicklung: Dem Datei-Upload (multipart/form-data). Wir werden im Laravel-Backend das legendäre Paket Spatie Media Library installieren, um hochgeladene Bilder automatisch zu komprimieren und WebP-Thumbnails zu generieren. Im Next.js-Frontend erweitern wir unser Formular um eine Drag-and-Drop Dropzone, senden die Bilddateien sicher über Sanctum an die API und nutzen schließlich die extrem performante next/image Komponente, um diese Bilder rasend schnell an unsere Blog-Leser auszuliefern. Wir bringen Farbe in dein CMS!
Hier geht es zu Teil 9: Bilder-Uploads mit Next.js und Laravel Spatie Media Library

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


