Um eine E-Mail-Adresse in Next.js zu validieren, müssen Sie drei Fragen
beantworten, und der App Router bietet Ihnen einen sauberen Ort, um jede einzelne
zu beantworten. Ist die Adresse korrekt geformt? Kann ihre Domain Mail empfangen?
Existiert das Postfach tatsächlich? Die erste ist ein zod-Schema, das Sie
zwischen einer Client-Komponente und dem Server teilen; die zweite ist eine
DNS-Abfrage; die dritte ist ein Netzwerkproblem, das Sie an eine
Verifizierungs-API übergeben. Die Falle besteht darin, bei der ersten Frage
stehenzubleiben — ein bestandenes z.string().email() im Browser ist eine
UX-Nettigkeit, keine Validierung, und es erreicht Ihre Datenbank nie intakt, denn
jeder kann daran vorbei posten.
Die kurze Antwort#
Validieren Sie mit einem gemeinsam genutzten zod-Schema für die Syntax sowohl
auf dem Client als auch auf dem Server, mit node:dns/promises für die MX-Abfrage
innerhalb eines Route Handlers und mit einer Verifizierungs-API für die
SMTP-Postfachprüfung — günstigste zuerst, mit Kurzschluss, sobald eine Ebene
entscheidend ist. Halten Sie jede Prüfung, die zählt, auf dem Server: Die
Client-Kopie dient dem sofortigen Feedback, und der API-Schlüssel, der die
Postfachprüfung authentifiziert, darf niemals an den Browser ausgeliefert werden.
Versuchen Sie nicht, aus einer Next.js-Funktion selbst SMTP-Verbindungen zu
öffnen, um Postfächer zu prüfen — der ausgehende Port 25 ist auf den meisten Hosts
blockiert (und in der Edge-Runtime gänzlich nicht verfügbar), und die Antwort
hängt von der Reputation der sendenden IP und von Greylisting ab, das Sie nicht
nachbauen wollen.
Ebene 1: Format-Validierung mit einem gemeinsam genutzten zod-Schema#
Next.js hat kein eigenes E-Mail-Primitiv, aber die idiomatische Formatprüfung ist
ein zod-Schema, und der Grund, warum zod hier so gut passt, ist, dass
dasselbe Schema im Browser und auf dem Server läuft. Definieren Sie es einmal:
// lib/email.ts
import { z } from "zod";
export const signupSchema = z.object({
email: z.string().email("Enter a valid email address."),
});
Parsen Sie in einer Client-Komponente beim Absenden dagegen, damit der Nutzer
sofort einen Fehler erhält, ohne einen Roundtrip. safeParse gibt ein
diskriminiertes Ergebnis zurück, das Sie ohne try/catch auslesen können:
"use client";
import { useState } from "react";
import { signupSchema } from "@/lib/email";
export function SignupForm() {
const [error, setError] = useState<string | null>(null);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const email = String(new FormData(e.currentTarget).get("email"));
// Instant feedback only — this is not a security boundary.
const parsed = signupSchema.safeParse({ email });
if (!parsed.success) {
setError(parsed.error.issues[0].message);
return;
}
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
setError(res.ok ? null : (await res.json()).error);
}
return (
<form onSubmit={onSubmit}>
<input name="email" type="email" autoComplete="email" required />
{error && <p role="alert">{error}</p>}
<button type="submit">Sign up</button>
</form>
);
}
Die Client-Prüfung ist es wert, vorhanden zu sein — sie spart bei offensichtlichen
Tippfehlern eine Anfrage —, aber sie ist das Gegenteil von vertrauenswürdig. Ein
Nutzer mit geöffnetem Netzwerk-Tab kann direkt an /api/signup posten und sie
vollständig überspringen, sodass der Server exakt dasselbe Schema erneut ausführen
muss, bevor er irgendetwas glaubt.
Ebene 2: Kann die Domain Mail empfangen?#
Dies ist die erste Prüfung, die der Browser nicht durchführen kann und zod nicht
durchführen wird: Eine Domain ohne MX-Einträge kann für niemanden Mail annehmen,
sodass eine einzige DNS-Abfrage tote Domains, falsch geschriebene Firmennamen und
erfundene TLDs ausschließt. Ein Next.js Route Handler läuft standardmäßig auf Node,
sodass Ihnen node:dns/promises ohne jede Abhängigkeit zur Verfügung steht:
// lib/email.ts (continued)
import { resolveMx } from "node:dns/promises";
export async function hasMailRoute(domain: string): Promise<boolean> {
try {
const mx = await resolveMx(domain);
return mx.length > 0;
} catch {
// ENOTFOUND / ENODATA → the domain doesn't exist or publishes no MX.
return false;
}
}
Ein Bestehen bedeutet hier „die Domain nimmt Mail an“, nicht „dieses Postfach existiert“ — aber ein Scheitern ist entscheidend und kostenlos, genau das, was Sie von einer günstigen Ebene wollen.
Ebene 3: Existiert das Postfach tatsächlich?#
Die Ebenen 1 und 2 können eine Adresse nur ausschließen. Eine Domain kann
perfekte MX-Einträge veröffentlichen und trotzdem kein Postfach an der Adresse
haben, die Sie in der Hand halten — noreply-9f2x@gmail.com ist gültige Syntax
auf einer aktiven Mail-Route und dennoch ein Postfach, das nie angelegt wurde. Ein
bestimmtes Postfach zu bestätigen bedeutet die SMTP-Zustellkonversation: sich mit
dem Mail-Host verbinden, RCPT TO absetzen, die Antwort lesen und die Verbindung
trennen, bevor irgendetwas gesendet wird.
Sie könnten das aus einer Node-Runtime heraus skripten, aber Sie sollten es nicht aus einer Next.js-Funktion heraus ausführen. Die meisten Cloud-Hosts blockieren den ausgehenden Port 25, die Antwort hängt von der Reputation der IP ab, von der aus Sie sich verbinden, und empfangende Server greylisten und drosseln unbekannte Absender — sodass eine Prüfung, die lokal funktioniert, in der Produktion still und leise scheitert oder Sie auf eine Blockliste bringt. Wie E-Mail-Verifizierung funktioniert durchläuft die vollständige Pipeline, Catch-all-Domains und alles. Dies ist die Ebene, die es sich zu delegieren lohnt.
Der Verify-Endpunkt von Qualisend führt die gesamte Pipeline auf
reputationsverwalteter Infrastruktur aus und gibt ein Urteil zurück. Rufen Sie ihn
serverseitig mit fetch auf und lesen Sie den Schlüssel aus einer
Umgebungsvariable, sodass er die Maschine nie verlässt:
// lib/email.ts (continued)
export async function verifyMailbox(email: string): Promise<string> {
const res = await fetch("https://api.qualisend.com/v1/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
// Don't block a real signup on our outage — treat a failure as "unknown".
if (!res.ok) return "unknown";
const { result } = await res.json();
return result.status; // "deliverable" | "risky" | "undeliverable" | "unknown"
}
Die Antwort ist ein { "result": { ... } }-Umschlag. Sie lesen result.status
neben einem score, einem reason und sub_flags für Merkmale wie Rollen- oder
Wegwerf-Adressen:
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
}
}
Schauen Sie in die API-Referenz für die genauen Feldformen; der
obige Helfer braucht nur status, um eine Adresse abzulehnen, die nicht
zugestellt wird. Beachten Sie, dass die Umgebungsvariable schlicht
QUALISEND_API_KEY heißt, nicht mit NEXT_PUBLIC_ präfixiert — dieses Präfix
ist es, was einen Wert in das Client-Bundle einbettet, und ein
Verifizierungsschlüssel im Browser ist ein Schlüssel, den jeder lesen und
verbrauchen kann.
Die Ebenen zusammensetzen, um eine E-Mail-Adresse in Next.js zu validieren#
Nähen Sie nun die drei Helfer in einer einzigen route.ts zusammen, günstigste
zuerst, sodass das API-Guthaben nur für eine Adresse ausgegeben wird, die Syntax
und MX-Prüfung bereits bestanden hat:
// app/api/signup/route.ts
import { NextResponse } from "next/server";
import { signupSchema, hasMailRoute, verifyMailbox } from "@/lib/email";
export async function POST(request: Request) {
// Layer 1 — re-validate on the server; the client check is not a boundary.
const parsed = signupSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: "Enter a valid email address." }, { status: 400 });
}
const { email } = parsed.data;
const domain = email.slice(email.lastIndexOf("@") + 1);
// Layer 2 — cheap MX gate, rules out dead domains before any API call.
if (!(await hasMailRoute(domain))) {
return NextResponse.json({ error: "That domain can't receive email." }, { status: 400 });
}
// Layer 3 — the SMTP mailbox probe, delegated to Qualisend.
if ((await verifyMailbox(email)) === "undeliverable") {
return NextResponse.json(
{ error: "We couldn't confirm a mailbox at this address." },
{ status: 400 },
);
}
// Every layer passed (or the probe was inconclusive) — create the account.
// await createUser(email);
return NextResponse.json({ ok: true });
}
Diese Reihenfolge ist der ganze Trick: Das Schema kurzschließt Müll, die lokale
DNS-Prüfung schließt tote Domains kostenlos aus, und die API wird nur für Adressen
getroffen, die beide durchlaufen haben. Der Handler lässt nur undeliverable hart
scheitern und lässt risky und unknown durch, sodass eine Grenzfall-Adresse
oder ein vorübergehendes API-Scheitern niemals einen echten Kunden abweist — Sie
können nachgelagert anhand von score oder eines sub_flags-Eintrags verzweigen,
statt die Registrierung zu blockieren.
Wenn Sie das Formular lieber absenden möchten, ohne Client-Fetch-Code zu
schreiben, funktioniert derselbe Rumpf in einer Server Action. Geben Sie
{ error } statt NextResponse zurück und konsumieren Sie es mit
useActionState:
"use server";
import { signupSchema, hasMailRoute, verifyMailbox } from "@/lib/email";
export async function signup(_prev: unknown, formData: FormData) {
const parsed = signupSchema.safeParse({ email: formData.get("email") });
if (!parsed.success) return { error: "Enter a valid email address." };
const { email } = parsed.data;
const domain = email.slice(email.lastIndexOf("@") + 1);
if (!(await hasMailRoute(domain))) return { error: "That domain can't receive email." };
if ((await verifyMailbox(email)) === "undeliverable") {
return { error: "We couldn't confirm a mailbox at this address." };
}
return { ok: true };
}
Dies ist dieselbe Drei-Ebenen-Form, die Sie auch im Node.js-Leitfaden finden — die Schichtung, nicht das Framework, ist es, was die Validierung zuverlässig macht. Führen Sie diese schnelle Pipeline beim Signup synchron aus und reservieren Sie tiefere Stapelprüfungen für die Listenbereinigung; der Leitfaden für Serverless-Signups zeigt das Echtzeitmuster von Anfang bis Ende, und der API-Vergleich stellt die Dienste gegenüber, die Ebene drei stützen können.
Häufig gestellte Fragen#
Reicht z.string().email() aus, um eine E-Mail-Adresse in Next.js zu validieren?#
Für die Syntax ja — z.string().email() ist die richtige Prüfung auf Ebene eins
und weit besser als eine selbstgebaute Regex. Aber sie validiert die Form: Sie
löst nie DNS auf und kontaktiert nie einen Mailserver, daher bedeutet ein Bestehen
„sieht aus wie eine E-Mail“, nicht „wird zugestellt“. Und eine reine
Browser-Prüfung lässt sich trivial umgehen, indem man direkt an Ihre Route postet.
Führen Sie das Schema erneut auf dem Server aus und ergänzen Sie dann eine
MX-Abfrage und eine SMTP-Postfachprüfung, bevor Sie der Adresse vertrauen.
Wo lege ich den Qualisend-API-Schlüssel in einer Next.js-App ab?#
In einer serverseitigen Umgebungsvariable namens QUALISEND_API_KEY — niemals in
einer mit dem Präfix NEXT_PUBLIC_. Das Präfix NEXT_PUBLIC_ bettet einen Wert
in das Client-Bundle ein, sodass ein Verifizierungsschlüssel mit diesem Präfix für
jeden lesbar ist, der Ihre Website öffnet. Lesen Sie
process.env.QUALISEND_API_KEY innerhalb eines Route Handlers oder einer Server
Action, wo er auf dem Server bleibt, und lassen Sie den Browser Ihren eigenen
Endpunkt aufrufen statt Qualisend direkt.
Sollte ich eine E-Mail in Next.js auf dem Client oder auf dem Server validieren?#
Beides, aber nur der Server zählt. Führen Sie das gemeinsam genutzte
zod-Schema in der Client-Komponente aus, um sofortiges Feedback bei Tippfehlern
zu geben, und führen Sie exakt dasselbe Schema erneut im Route Handler oder in der
Server Action aus, denn die Client-Prüfung lässt sich vollständig überspringen.
Die DNS- und SMTP-Ebenen sind naturgemäß serverseitig — sie benötigen Node-APIs
und einen geheimen Schlüssel — sodass der Browser sie nie zu Gesicht bekommt.
Kann ich die DNS/MX-Prüfung in der Edge-Runtime ausführen?#
Nein. node:dns/promises benötigt die Node.js-Runtime, die Route Handler
standardmäßig verwenden; eine Route mit export const runtime = "edge" hat kein
dns-Modul. Wenn Sie Edge brauchen, lassen Sie das lokale MX-Gate weg und lassen
Sie Qualisend DNS als Teil des Verify-Aufrufs auflösen — seine Pipeline führt
bereits die MX-Abfrage und die SMTP-Prüfung durch, sodass der einzelne API-Aufruf
die Ebenen zwei und drei abdeckt.
Bereit, die Postfach-Ebene hinzuzufügen? Werfen Sie eine syntaktisch perfekte
Adresse in den kostenlosen E-Mail-Checker, um zuzusehen,
wie eine zod-genehmigte Zeichenkette als undeliverable zurückkommt, und
verdrahten Sie dann dasselbe Urteil in Ihren Route Handler mit den
Copy-Paste-Beispielen in der API-Referenz.