Erstellung robuster und typsicherer Formulare mit Zod-form-data in Remix und Next.js
Grace Collins
Solutions Engineer · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der Webentwicklung bleibt die Erstellung benutzerfreundlicher und zuverlässiger Formulare eine grundlegende Herausforderung. Wenn Anwendungen komplexer werden, sind die Gewährleistung der Datenintegrität, die Bereitstellung aussagekräftiger Benutzerfeedbacks und die Aufrechterhaltung einer reibungslosen Entwicklererfahrung von größter Bedeutung. Traditionelle Ansätze beinhalten oft das Jonglieren mit clientseitiger Validierung, serverseitiger Validierung und manuellen Typzertifizierungen, was zu einem fragmentierten und fehleranfälligen Prozess führt. Dies kann zu subtilen Fehlern, Inkonsistenzen zwischen Client und Server und einer Entwicklererfahrung führen, die sich eher wie das Ringen mit Typen als wie das Erstellen von Funktionen anfühlt.
Das Aufkommen moderner Full-Stack-Frameworks wie Remix und Next.js, gepaart mit leistungsstarken Schema-Validierungsbibliotheken, bietet eine überzeugende Lösung. Insbesondere die Integration von zod-form-data bringt eine neue Ebene der Komplexität in die Formularverarbeitung und ermöglicht sofort standardmäßig durchgängige Typsicherheit und progressive Verbesserung. Dieser Ansatz optimiert nicht nur die Entwicklung, sondern verbessert auch die Robustheit und Zuverlässigkeit Ihrer Webanwendungen erheblich. Dieser Artikel befasst sich damit, wie wir zod-form-data in Remix und Next.js nutzen können, um diesen wünschenswerten Zustand zu erreichen und über die übliche clientseitige Validierung hinaus zu einer wirklich typsicheren und progressiv verbesserten Formularerfahrung zu gelangen.
Verstehen der Kernkomponenten
Bevor wir uns mit den Implementierungsdetails befassen, definieren wir kurz die Schlüsseltechnologien und Konzepte, die für unsere Diskussion zentral sind:
- Remix / Next.js: Dies sind Full-Stack-React-Frameworks.
- Remix: Legt Wert auf Webstandards, serverseitiges Rendering (SSR) und verschachteltes Routing, was integrierte Mechanismen für Formularübermittlungen und Datenmutationen bietet. Sein Action/Loader-Paradigma eignet sich besonders gut für die Verarbeitung von Formulardaten.
- Next.js: Bietet leistungsstarke Funktionen wie SSR, Static Site Generation (SSG) und API-Routen, was es vielseitig für verschiedene Anwendungsarchitekturen macht. Seine API-Routen dienen als hervorragendes Backend für die Verarbeitung von Formularübermittlungen.
- Progressive Enhancement: Eine Strategie für die Webentwicklung, die darin besteht, eine Basis aus Kerninhalten und Funktionalität zu erstellen, die für alle Benutzer zugänglich ist, und dann schrittweise Ebenen von Präsentation und Funktionalität für Benutzer mit leistungsfähigeren Browsern hinzuzufügen. Im Kontext von Formularen bedeutet dies, dass ein Formular auch bei deaktiviertem JavaScript funktionieren sollte, aber erweiterte Funktionen (wie sofortige Validierung) bietet, wenn JavaScript verfügbar ist.
- End-to-End Type Safety: Sicherstellung, dass Datentypen durchgängig über alle Ebenen einer Anwendung hinweg erzwungen und validiert werden, von der Benutzeroberfläche (clientseitig) bis zum Backend (serverseitig) und der Datenbank. Dies minimiert typbezogene Fehler, verbessert die Code-Wartbarkeit und bietet starke Garantien für die Datenkonsistenz.
- Zod: Eine TypeScript-first Schema-Deklarations- und Validierungsbibliothek. Sie ermöglicht es Entwicklern, Schemas für beliebige Datenstrukturen zu definieren, die dann zur Validierung eingehender Daten und zur Ableitung von TypeScript-Typen verwendet werden können. Zods leistungsstarke Inferenzfähigkeiten sind ein Eckpfeiler der durchgängigen Typsicherheit.
zod-form-data: Ein Zod-Präprozessor, der speziellFormData-Objekte verarbeitet. Er ermöglicht es Ihnen, ein Zod-Schema zu definieren, um über HTML-Formulare übermittelte Daten zu validieren und zu transformieren, wobei Datei-Uploads, Kontrollkästchen und Mehrfachauswahlen ordnungsgemäß behandelt werden. Wichtig ist, dass es Zeichenwerte ausFormDatabasierend auf dem Zod-Schema automatisch in ihre korrekten Typen (z. B. Zahlen, Booleans) umwandeln kann.
Erzielung von Progressive Enhancement und End-to-End Type Safety
Die Kernidee ist die Definition eines einzigen, autoritativen Zod-Schemas, das die erwartete Struktur und die Typen unserer Formulardaten darstellt. Dieses Schema wird dann sowohl auf dem Client zur Bereitstellung sofortigen Feedbacks als auch auf dem Server zur rigorosen Validierung eingehender Übermittlungen verwendet. zod-form-data überbrückt die Lücke, indem es diesem Zod-Schema ermöglicht, das rohe FormData-Objekt von einer HTML-Formularübermittlung direkt zu verarbeiten.
Lassen Sie uns dies anhand praktischer Beispiele in Remix und Next.js veranschaulichen.
Beispiel in Remix
Remix' action-Funktion ist ein natürlicher Anknüpfungspunkt für die Verarbeitung von Formularübermittlungen. Wir können unser Zod-Schema einmal definieren und es innerhalb der Aktion verwenden.
// app/routes/newsletter.tsx import { ActionFunctionArgs, json } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; import { z } from "zod"; import { zfd } from "zod-form-data"; // 1. Definiere das Zod-Schema für unsere Formulardaten const newsletterSchema = zfd.formData({ email: zfd.text(z.string().email("Ungültige E-Mail-Adresse")), source: zfd.text(z.string().optional()), acceptTerms: zfd.checkbox(), }); type NewsletterData = z.infer<typeof newsletterSchema>; export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); try { // 2. Parse und validiere die Formulardaten anhand des Schemas const data = newsletterSchema.parse(formData); // 3. Wenn die Validierung erfolgreich ist, verarbeite die Daten console.log("Newsletter-Anmeldedaten:", data); // In einer echten Anwendung würden Sie dies in einer Datenbank speichern, eine E-Mail senden usw. return json({ success: true, message: "Danke für Ihre Anmeldung!" }); } catch (error) { // 4. Wenn die Validierung fehlschlägt, gib Fehler zurück if (error instanceof z.ZodError) { const errors = error.flatten(); return json({ success: false, errors: errors.fieldErrors }, { status: 400 }); } return json({ success: false, message: "Ein unerwarteter Fehler ist aufgetreten." }, { status: 500 }); } } export default function NewsletterSignup() { const actionData = useActionData<typeof action>(); return ( <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold mb-4">Abonnieren Sie unseren Newsletter</h1> <Form method="post" className="space-y-4"> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700">E-Mail:</label> <input type="email" id="email" name="email" required className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> {actionData?.errors?.email && ( <p className="mt-1 text-sm text-red-600">{actionData.errors.email[0]}</p> )} </div> <div> <label htmlFor="source" className="block text-sm font-medium text-gray-700">Woher kennen Sie uns? (Optional)</label> <input type="text" id="source" name="source" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> </div> <div className="flex items-center"> <input type="checkbox" id="acceptTerms" name="acceptTerms" required className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" /> <label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-900"> Ich akzeptiere die Allgemeinen Geschäftsbedingungen </label> {actionData?.errors?.acceptTerms && ( <p className="ml-2 text-sm text-red-600">{actionData.errors.acceptTerms[0]}</p> )} </div> <button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Abonnieren </button> </Form> {actionData?.success && ( <p className="mt-4 text-green-600">{actionData.message}</p> )} {actionData?.success === false && !actionData.errors && ( <p className="mt-4 text-red-600">{actionData.message}</p> )} </div> ); }
Erklärung:
- Schema-Definition: Wir definieren
newsletterSchemamitzfd.formData. Beachten Sie, wiezfd.textfür Textfelder undzfd.checkboxfür Kontrollkästchen verwendet wird.zfd.checkboxwandelt den Wert des Kontrollkästchens korrekt in einen Boolean um. - Serverseitige Validierung (Remix
action): Innerhalb deraction-Funktion erhalten wir dieformDatavon der Anfrage.newsletterSchema.parse(formData)versucht, dieFormDatazu validieren und in die definierten Typen zu konvertieren. Wenn die Validierung fehlschlägt, wird einZodErrorausgelöst, den wir abfangen, um spezifische Feldfehler zurückzugeben. - Progressive Enhancement: Wenn JavaScript deaktiviert ist, übermittelt das Formular direkt an den
action-Endpunkt, und die serverseitige Validierung funktioniert weiterhin und gibt die entsprechenden HTTP-Statuscodes und Fehlermeldungen zurück. - Clientseitiges Feedback mit Typsicherheit:
useActionDatastellt die Validierungsergebnisse vom Server der Benutzeroberfläche zur Verfügung. Da der Rückgabetyp vonactionbekannt ist, istactionDatavollständig typisiert, sodass wir Fehler für bestimmte Felder vertrauensvoll anzeigen können.
Beispiel in Next.js
Für Next.js verwenden wir typischerweise API-Routen oder Server Actions (eingeführt in Next.js 13.4+), um Formularübermittlungen zu verarbeiten. Wir zeigen ein Beispiel mit API-Routen, das allgemeiner anwendbar ist.
// pages/api/newsletter.ts (API-Route) import type { NextApiRequest, NextApiResponse } from 'next'; import { z } from 'zod'; import { zfd } from 'zod-form-data'; // 1. Definiere das Zod-Schema für unsere Formulardaten (wie bei Remix) const newsletterSchema = zfd.formData({ email: zfd.text(z.string().email("Ungültige E-Mail-Adresse")), source: zfd.text(z.string().optional()), acceptTerms: zfd.checkbox(), }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method Not Allowed' }); } try { // Next.js API-Routen stellen FormData in req.body für application/x-www-form-urlencoded nicht direkt bereit // Wir simulieren formData für Einfachheit, aber in einem realen Szenario mit tatsächlichem FormData, // benötigen Sie möglicherweise ein Middleware wie `next-connect` oder `formidable`, um es korrekt zu parsen, // oder stellen Sie sicher, dass Ihr Client `application/json` sendet, wenn keine Dateien verwendet werden. // Für einfache Formulare wird `req.body` direkt zugeordnet, wenn `Content-Type: application/x-www-form-urlencoded` // oder application/json für `fetch`-basierte Übermittlungen. // Zur Demonstration wandeln wir `req.body` für den Fall, dass es sich um ein Objekt handelt, in ein simuliertes `FormData`-Objekt um, // oder `req.body` direkt parsen, als wäre es ein einfaches Objekt von `application/x-www-form-urlencoded`. // Eine robustere Methode zur Verarbeitung von FormData in Next.js API-Routen: // Sie würden normalerweise eine Bibliothek wie 'formidable' oder 'multer' verwenden, um den multipart/form-data Stream zu parsen. // Für application/x-www-form-urlencoded ist req.body bereits geparst. // Passen wir unser Schema an, um das bereits geparste `req.body` zu parsen, wenn es application/x-www-form-urlencoded ist // oder nehmen Sie eine Struktur an, die `zfd.formData` nach manuellem Parsen erwartet. // Der Einfachheit halber gehen wir davon aus, dass `req.body` ein Objekt ist, das direkt Formularfelder darstellt // das zfd dann verarbeiten kann, als ob es FormData wäre. Dies funktioniert für application/x-www-form-urlencoded. const formDataLikeObject = new FormData(); for (const key in req.body) { // Behandeln Sie array-ähnliche Werte wie mehrere Kontrollkästchen oder Auswahloptionen if (Array.isArray(req.body[key])) { req.body[key].forEach((item: string) => formDataLikeObject.append(key, item)); } else { formDataLikeObject.append(key, req.body[key]); } } // 2. Parse und validiere die Formulardaten anhand des Schemas const data = newsletterSchema.parse(formDataLikeObject); // 3. Wenn die Validierung erfolgreich ist, verarbeite die Daten console.log("Newsletter-Anmeldedaten:", data); // In einer echten Anwendung würden Sie dies in einer Datenbank speichern, eine E-Mail senden usw. return res.status(200).json({ success: true, message: "Danke für Ihre Anmeldung!" }); } catch (error) { // 4. Wenn die Validierung fehlschlägt, gib Fehler zurück if (error instanceof z.ZodError) { const errors = error.flatten(); return res.status(400).json({ success: false, errors: errors.fieldErrors }); } return res.status(500).json({ success: false, message: "Ein unerwarteter Fehler ist aufgetreten." }); } }
// pages/newsletter-signup.tsx (Clientseitige Seite) import { useState } from 'react'; import { z } from 'zod'; // Importiere Zod für clientseitige Validierungshinweise // Verwende das gleiche Schema für die clientseitige Validierung, um Konsistenz zu gewährleisten const newsletterClientSchema = z.object({ email: z.string().email("Ungültige E-Mail-Adresse"), source: z.string().optional(), // Hinweis: zfd.checkbox behandelt implizit 'on'/'off' oder fehlende Einträge. // Für reine clientseitige Zod würden Sie direkt auf einen Boolean prüfen. acceptTerms: z.boolean().refine(val => val === true, "Sie müssen die Nutzungsbedingungen akzeptieren"), }); export default function NewsletterSignupPage() { const [status, setStatus] = useState<{ success: boolean; message?: string; errors?: Record<string, string[]> } | null>(null); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setStatus(null); const formData = new FormData(event.currentTarget); const formObject = Object.fromEntries(formData.entries()); // Clientseitige Vor-Validierung für sofortiges Feedback try { newsletterClientSchema.parse({ ...formObject, acceptTerms: formData.get('acceptTerms') === 'on' // Kontrollkästchenwert konvertieren }); } catch (error) { if (error instanceof z.ZodError) { setStatus({ success: false, errors: error.flatten().fieldErrors }); return; } } try { const response = await fetch('/api/newsletter', { method: 'POST', // Die Verwendung von FormData direkt funktioniert am besten für Dateien, aber für einfache Textfelder // sind `application/x-www-form-urlencoded` oder `application/json` ebenfalls üblich. // Für dieses Beispiel wird `formData` indirekt als `multipart/form-data` gesendet. // Wenn Sie explizit `application/x-www-form-urlencoded` wünschen, würden Sie formData in URLSearchParams konvertieren body: formData, }); const data = await response.json(); setStatus(data); } catch (error) { setStatus({ success: false, message: "Netzwerkfehler. Bitte versuchen Sie es erneut." }); } }; return ( <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold mb-4">Abonnieren Sie unseren Newsletter</h1> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700">E-Mail:</label> <input type="email" id="email" name="email" required className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> {status?.errors?.email && ( <p className="mt-1 text-sm text-red-600">{status.errors.email[0]}</p> )} </div> <div> <label htmlFor="source" className="block text-sm font-medium text-gray-700">Woher kennen Sie uns? (Optional)</label> <input type="text" id="source" name="source" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> </div> <div className="flex items-center"> <input type="checkbox" id="acceptTerms" name="acceptTerms" required className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" /> <label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-900"> Ich akzeptiere die Allgemeinen Geschäftsbedingungen </label> {status?.errors?.acceptTerms && ( <p className="ml-2 text-sm text-red-600">{status.errors.acceptTerms[0]}</p> )} </div> <button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" > Abonnieren </button> </form> {status?.success && ( <p className="mt-4 text-green-600">{status.message}</p> )} {status?.success === false && !status.errors && ( <p className="mt-4 text-red-600">{status.message}</p> )} </div> ); }
Erklärung:
- Schema-Definition: Das
newsletterSchemableibt identisch. Dies ist entscheidend für die durchgängige Typsicherheit. - Serverseitige Validierung (Next.js API-Route):
req.bodyvonNextApiRequestliefert fürmultipart/form-data-Anfragen nicht automatisch einFormData-Objekt. Für einfache übermittelteapplication/x-www-form-urlencoded-Formulare wirdreq.bodyin ein Objekt geparst.- Wir rekonstruieren manuell ein
FormData-Objekt ausreq.body, damitzfd.formDatanahtlos funktioniert. In einer Produktionsumgebung mit tatsächlichemFormData(z. B. für Datei-Uploads) würden Sie ein Middleware wieformidableverwenden, um denmultipart/form-data-Stream zu parsen und dann die geparsten Felder anzfd.formDatazu übergeben. - Die Fehlerbehandlung ist ähnlich wie bei Remix, wobei JSON-Antworten zurückgegeben werden.
- Clientseitige Validierung und Progressive Enhancement:
- Wir verwenden die gleiche
z-Schemaform für die clientseitige Validierung für sofortiges Feedback und konvertieren die Kontrollkästchenwerte entsprechend. - Wenn das Formular übermittelt wird, sendet
fetchdieFormDataan die API-Route. - Wenn JavaScript deaktiviert ist, fällt der Browser elegant auf eine herkömmliche Formularübermittlung zurück, aber da es sich um eine clientseitige Route handelt, funktioniert dies nicht auf die gleiche Weise wie bei "progressiver Verbesserung" wie bei einer Remix-Aktion, bei der das Formular buchstäblich an dieselbe Route postet. Für eine echte progressive Verbesserung in Next.js ohne Server Actions benötigen Sie möglicherweise eine dedizierte Seite für
POST-Anfragen oder eine Seitenneuladung bei Fehlern. Next.js Server Actions vereinfachen dies drastisch und machen das Verhalten Remix sehr ähnlich. - Clientseitige Validierung bietet sofortiges Benutzerfeedback, während serverseitige Validierung als definitive Absicherung dient.
- Wir verwenden die gleiche
Fazit
Durch die Integration von zod-form-data mit Zod etablieren wir ein robustes Muster für die Formularverarbeitung in Remix und Next.js. Dieser Ansatz zentralisiert die Definition von Formularschemata, gewährleistet durchgängige Typsicherheit von der Benutzeroberfläche bis zum Backend und unterstützt von Natur aus progressive Verbesserung. Entwickler profitieren von reduziertem Boilerplate-Code, Compile-Zeit-Fehlerüberprüfung und einer konsistenten Validierungsgeschichte in ihrer Anwendung, was letztendlich zu zuverlässigeren und wartbareren Formularen führt. Diese leistungsstarke Kombination verbessert die Entwicklererfahrung und die Qualität der Benutzerinteraktion mit Formularen erheblich.

