Nextjs Contact Form: E-Mail senden und Spam blocken
Ein Nextjs Contact Form sieht nach zehn Minuten Arbeit aus: drei Felder, ein Button, eine E-Mail im Postfach. Dann kommt die erste Spamwelle, ein Besucher verliert wegen eines Validierungsfehlers eine lange Nachricht, und du merkst, dass dein SMTP-Passwort nur einen Import vom Client-Bundle entfernt war. Dieses Tutorial baut ein Next.js-Kontaktformular im App Router von Next.js 16, das mit zod validiert, die E-Mail aus einer Server Action sendet, Bots per Honeypot aussortiert, Anfragen pro IP begrenzt und behält, was der Besucher getippt hat, wenn etwas schiefgeht. Zum Schluss ersetzt die kostenlose Vorlage Contact Page das nackte Formular.
Was braucht ein Next.js-Kontaktformular?
Fünf Teile. Die ersten drei ergeben ein funktionierendes Formular, die letzten zwei entscheiden, ob es echte Besucher übersteht:
- Ein Schema, das festlegt, was eine gültige Nachricht ist, und das jede Stelle nutzt, die das Formular liest.
- Eine Server Action, die die Formulardaten parst, die E-Mail sendet und Feldfehler zurückgibt.
- Ein Client-Formular mit Labels, Fehlern direkt am Feld, einem Button mit Ladezustand und einer Bestätigung.
- Spamschutz, der keine Menschen bestraft: ein Honeypot und ein Rate Limit.
- Fehlertoleranz: Der Text des Besuchers übersteht einen gescheiterten Versand oder einen Validierungsfehler.
Der eigentliche E-Mail-Versand, also der Schritt, den Leute mit „nextjs contact form send email“ suchen, ist etwa zehn Zeilen lang. Die Teile 4 und 5 brauchen mehr Überlegung, und hinter Teil 5 steckt ein Verhalten von React 19, das das Formular nach einem gescheiterten Versand leert.
Soll ich eine Server Action oder eine API-Route nehmen?
Nimm eine Server Action, solange nichts außer deinem eigenen Formular den Endpunkt braucht. Mit einer Action schickt <form action={...}> die Daten ohne fetch-Aufruf an den Server, das Formular funktioniert schon, bevor JavaScript geladen ist, und useActionState gibt das Ergebnis direkt an die Komponente zurück. Der Forms-Guide von Next.js (opens in a new tab) nutzt genau dieses Muster mit zod für die Validierung (geprüft am 5. November 2026).
Ein Route Handler (app/api/contact/route.ts) ist weiterhin die bessere Wahl, wenn eine Mobile App, ein Webhook oder ein Formular-Builder von Drittanbietern an denselben Endpunkt sendet. In beiden Fällen bleiben deine E-Mail-Zugangsdaten auf dem Server. Eine Warnung gilt für beide: Eine Action ist ein öffentlicher HTTP-Endpunkt. Jeder kann sie mit beliebigen Daten aufrufen, also validierst du auf dem Server, auch wenn der Client schon validiert.
Wie validiere ich ein Kontaktformular mit zod?
Leg das Schema in eine eigene Datei ohne "use server", damit die Action und jeder Client-Code es importieren können. Trimm die Werte vor der Prüfung, begrenz jede Länge und lehn Zeilenumbrüche im Namen ab, denn der landet im Betreff einer E-Mail.
Das ist zod 4 (die aktuelle Version im November 2026): z.email() ist ein eigenständiges Schema auf oberster Ebene, und z.flattenError() macht aus einem gescheiterten Parse { fieldErrors: { name?: string[], ... } }, was sich eins zu eins auf die Formularfelder abbilden lässt.
Wie sende ich E-Mails aus einem Next.js-Kontaktformular?
Jeder Anbieter funktioniert. Dieses Beispiel nutzt Nodemailer (opens in a new tab) mit normalem SMTP (npm install nodemailer und npm install -D @types/nodemailer), es funktioniert also mit deinem eigenen Mailserver, Amazon SES, Postmark oder alles andere, was SMTP spricht. Der Import von server-only sorgt dafür, dass jeder Versuch, diese Datei in eine Client-Komponente zu ziehen, den Build abbricht.
Setz die Adresse des Besuchers nie in from. Die SPF- und DKIM-Einträge deiner Domain decken sie nicht ab, die DMARC-Ausrichtung schlägt fehl, und die Nachricht landet im Spam oder wird abgelehnt. Mit replyTo antwortest du genauso mit einem Klick.
Wie halte ich Spam mit einem Honeypot fern?
Ein Honeypot ist ein Textfeld, das Menschen nie sehen und einfache Bots ausfüllen, weil sie schlicht alles ausfüllen. Hat es einen Wert, tut die Action so, als hätte alles geklappt, und sendet nichts. Das Vortäuschen ist wichtig: Ein Bot, der einen Fehler sieht, versucht es mit anderen Eingaben noch einmal.
Wie baue ich ein Rate Limit für eine Server Action?
Zähl die Anfragen pro IP in einem gleitenden Zeitfenster. Die Version unten hält die Zähler im Arbeitsspeicher, und das reicht für einen einzelnen, dauerhaft laufenden Node-Server. Läuft die App serverless oder auf mehreren Instanzen, hat jede Kopie ihren eigenen Speicher. Dann gehört der Zähler nach Redis oder in deine Datenbank, zum Beispiel mit dem Paket @upstash/ratelimit von Upstash.
Wie setze ich alles in einer Server Action zusammen?
Eine Action verbindet die Teile: zuerst der Honeypot, dann das Rate Limit, dann die Validierung, dann der Versand. Jeder Fehlerfall gibt die eingegebenen Werte zurück. Warum, erklärt der nächste Abschnitt.
Vertrau x-forwarded-for nur, wenn dein Hoster oder Reverse Proxy den Header setzt; an einen nackten Node-Server kann jeder diesen Header direkt schicken.
Wie baue ich das Formular mit React und Tailwind?
Das Formular ist eine Client-Komponente, die useActionState aufruft. Diesen Schritt suchen Leute mit „react contact form tailwind“: Klassen aus Tailwind v4 für das Layout und Komponenten, die das Styling der Felder schon mitbringen. Für die Felder nutzt das Beispiel Input und Textarea von Wingo UI. Beide rendern über die gemeinsame Anatomie von Field das Label, die Beschreibung und unter dem Feld einen animierten Fehler, und die ids und aria-invalid sind schon verdrahtet. Der Toast bestätigt den Versand, ohne das Layout zu verschieben.
So verhalten sich die Fehler im Formular. Drück „Continue“ mit einem zu kurzen Titel und sieh zu, wie die Meldung den Hinweis ersetzt. Tipp dann ein Zeichen, und sie gleitet wieder weg.
$ npx wingo-ui@latest add fieldHäng den Toaster einmal mit <Toaster /> aus @/components/ui/toast ins Root-Layout und render das Formular dann aus einer Server-Seite:
Warum leert sich das Formular nach einem Fehler?
Weil React 19 es zurücksetzt. Laut der Doku zu Formularen in React (opens in a new tab) (geprüft am 5. November 2026) werden alle unkontrollierten Felder eines Formulars zurückgesetzt, nachdem die Action erfolgreich war. „Erfolgreich“ heißt hier nur, dass die Action keine Exception geworfen hat, also zählt auch eine Action, die Validierungsfehler zurückgibt. Ohne die Rückgabe von values verliert ein Besucher, der sich bei der E-Mail vertippt, eine Nachricht mit 600 Zeichen. Weil die Werte als defaultValue zurückkommen, stellt das Zurücksetzen wieder her, was er getippt hat. Nach einem echten Versand ist values leer, und das Formular leert sich, wie es soll.
noValidate schaltet die Fehlerblasen des Browsers ab, damit jede Meldung aus dem zod-Schema kommt, an derselben Stelle und im selben Stil. Die Props required zeichnen trotzdem die Pflichtfeld-Markierung und setzen aria-required.
Gibt es eine Vorlage für ein Next.js-Kontaktformular?
Ja, und sie ist kostenlos. Wer nach „next js contact form template“ sucht und eine ganze Kontaktseite als Vorlage statt eines nackten Formulars will, bekommt mit der Vorlage Contact Page von Wingo UI die komplette Seite: Themenkarten (Sales, Support, Partnerships, Press), Name, E-Mail, Firma, Firmengröße und eine Nachricht mit Live-Zeichenzähler, eine Einwilligungs-Checkbox, kopierbare Kanäle für E-Mail und Telefon, Öffnungszeiten mit Live-Status (offen oder geschlossen), Büros auf einer gezeichneten Karte, ein FAQ und eine Logo-Reihe. Neben der FAQ-Seite ist sie eine der beiden Vorlagen im kostenlosen Angebot.
Füll das Formular aus und schick es ab. Die Demo wartet 900 ms und zeigt dann die Bestätigung; ist ein Feld leer, springt der Fokus zum ersten Problem.
$ npx wingo-ui@latest add contact-pageDie Vorlage validiert im Client und ruft onSubmit(values) mit { name, email, company, companySize, topic, message, consent } auf. Solange das Promise läuft, dreht sich der Spinner im Button; wird es abgelehnt, zeigt die Vorlage ihre Meldung „didn't go through“. Auf dem Server brauchst du also eine zweite Action in derselben Datei, die ein Objekt nimmt und bei einem Fehler wirft. Die Vorlage verlangt außerdem mindestens 20 Zeichen in der Nachricht, und das gemeinsame Schema stimmt damit überein:
Pack die Vorlage in eine kleine Client-Datei. Die Route bleibt eine Server-Seite, die metadata exportieren kann, und der Wrapper ist der Ort für Props, die eine Server-Seite nicht übergeben kann, etwa Labels als Funktionen wie successDescription:
Render dann <Contact /> in app/contact/page.tsx statt <ContactForm />. Die Vorlage hat kein Honeypot-Feld, hier tragen also das Rate Limit und das Schema auf dem Server den Spamschutz; die Datei gehört dir, falls du eins ergänzen willst. Ersetz vor dem Launch die Beispielinhalte, die noch zu sehen sind: das FAQ (übergib faq oder setz showFaq={false}) und, falls du sie wieder einschaltest, die Acme-Büros und die Logo-Reihe, deren Titel 12.000 Teams behauptet, die nicht deine sind. Wenn du die Themenkarten änderst, änderst du das z.enum in pageSchema gleich mit. Auf Smartphones öffnet sich das Select für die Firmengröße als Bottom Sheet und die Kanäle rutschen unter das Formular, die Seite braucht also keine eigene Mobile-Arbeit.
Was sollte ich prüfen, bevor die Kontaktseite live geht?
- Zustellbarkeit. Sende von einer Domain, für die SPF, DKIM und DMARC bei deinem Anbieter eingerichtet sind, und teste, dass eine Antwort beim Besucher ankommt.
- Secrets.
SMTP_PASSsteht nur in den Umgebungsvariablen des Servers. Mitimport "server-only"im Mailer bricht ein Import aus dem Client den Build ab. - Missbrauch. Schick das Formular sechsmal in einer Minute ab und prüf, dass der sechste Versuch abgelehnt wird. Füll den Honeypot über die Dev Tools aus und prüf, dass nichts ankommt.
- Fehlertoleranz. Stopp den SMTP-Server, sende eine lange Nachricht und prüf, dass der Text mit einem Fehler-Toast im Formular stehen bleibt.
- Datenschutz. Verlink deine Datenschutzerklärung unter dem Formular (
privacyHrefin der Vorlage) und schreib dazu, was du mit der Adresse machst.
Weitere Screens, Schritt für Schritt gebaut, findest du in der Kategorie Build it, angefangen beim Tutorial zum Next.js Admin Dashboard. Für längere Formulare gehen der Guide zu mehrstufigen Formularen in React und der Guide zu Formular-Komponenten in React tiefer auf Felder ein, und das Tutorial zur shadcn Login Page nutzt dasselbe Muster mit Server Actions für die Anmeldung.
Wie geht es weiter?
Fang mit der Vorlage an, wenn du heute eine fertige Seite willst: npx wingo-ui@latest add contact-page, onSubmit mit der Action von oben verbinden und die Beispielinhalte ersetzen. Contact Page, Field, Input, Textarea, Button und Toast gehören alle zum kostenlosen Angebot. Ein kostenloses Konto (nur E-Mail, keine Karte) reicht also, um jede Datei von Wingo UI aus diesem Tutorial zu installieren.
Häufige Fragen
Wie sende ich aus einem Next.js-Kontaktformular eine E-Mail?
Schick das Formular an eine Server Action, validiere die Felder dort mit zod und ruf aus der Action deinen E-Mail-Anbieter auf: einen SMTP-Transport wie Nodemailer oder das SDK eines Anbieters. Die Zugangsdaten liegen in Umgebungsvariablen, die nur der Server sieht, und die Adresse des Besuchers setzt du als replyTo, nie als from.
Server Action oder API-Route für ein Kontaktformular in Next.js?
Im App Router ist die Server Action der kürzere Weg: Das Formular schickt seine Daten ohne fetch-Aufruf dorthin, es funktioniert schon, bevor JavaScript geladen ist, und Feldfehler gehen direkt an useActionState zurück. Ein Route Handler bleibt die richtige Wahl, wenn auch andere Clients denselben Endpunkt aufrufen, etwa eine Mobile App oder ein Webhook.
Warum leert mein Next.js-Formular nach einem Validierungsfehler alle Felder?
React 19 setzt unkontrollierte Felder zurück, sobald eine Form Action ohne Exception endet, und das Zurückgeben von Validierungsfehlern zählt als Ende ohne Exception. Gib die eingegebenen Werte aus der Action zurück und reich sie als defaultValue weiter, dann stellt das Zurücksetzen genau das wieder her, was der Besucher getippt hat.
Wie halte ich Spam ohne Captcha von einem Kontaktformular fern?
Mit einem Honeypot: einem Textfeld, das Menschen nicht sehen und Bots ausfüllen. Jede Anfrage, in der es einen Wert hat, verwirfst du. Kombinier das mit einem Rate Limit pro IP und Validierung auf dem Server; ein Captcha brauchst du erst, wenn trotzdem noch Spam durchkommt.
Gibt es eine kostenlose Vorlage für eine Kontaktseite in Next.js?
Ja. Die Vorlage Contact Page von Wingo UI gehört zum kostenlosen Angebot: ein validiertes Formular mit Themenkarten, kopierbaren Kanälen, Öffnungszeiten, Büros auf einer Karte und einem FAQ. Installier sie mit npx wingo-ui@latest add contact-page und übergib eine Server Action an onSubmit.
- Next.js
- Kontaktformular
- Server Actions
- Zod
- Tailwind CSS

