Skip to content
Commencez avec 100 crédits de vérification gratuits
Qualisend
Tous les articles
Ingénierie / 7 mai 2026

Valider une adresse e-mail dans Next.js

8 minutes read

Qualisend team
Une fenêtre d'éditeur de code intitulée route.ts montrant trois couches de validation Next.js de plus en plus fines — un schéma d'e-mail zod, une résolution MX et un appel à l'API de vérification — se terminant par un badge vert « deliverable ».

Pour valider une adresse e-mail dans Next.js, vous devez répondre à trois questions, et l'App Router vous offre un endroit propre pour répondre à chacune. L'adresse est-elle correctement formée ? Son domaine peut-il recevoir du courrier ? La boîte aux lettres existe-t-elle réellement ? La première est un schéma zod que vous partagez entre un Client Component et le serveur ; la deuxième est une résolution DNS ; la troisième est un problème réseau que vous confiez à une API de vérification. Le piège consiste à s'arrêter à la première question — un contrôle z.string().email() réussi dans le navigateur est un confort d'UX, pas une validation, et il n'atteint jamais votre base de données intact, car n'importe qui peut le court-circuiter en envoyant directement une requête POST.

La réponse courte#

Validez avec un schéma zod partagé pour la syntaxe, à la fois côté client et côté serveur, node:dns/promises pour la résolution MX à l'intérieur d'un Route Handler, et une API de vérification pour le contrôle SMTP de la boîte aux lettres — la moins coûteuse d'abord, en court-circuitant dès qu'une couche est décisive. Gardez sur le serveur chaque contrôle qui compte : la copie côté client sert au retour instantané, et la clé d'API qui authentifie le test de la boîte aux lettres ne doit jamais parvenir au navigateur. N'essayez pas d'ouvrir vous-même des connexions SMTP depuis une fonction Next.js pour tester les boîtes aux lettres — le port 25 sortant est bloqué sur la plupart des hébergeurs (et totalement indisponible sur le runtime Edge), et la réponse dépend de la réputation de l'IP d'envoi et du greylisting que vous ne voulez pas réimplémenter.

Couche 1 : validation du format avec un schéma zod partagé#

Next.js n'a pas de primitive e-mail propre, mais le contrôle de format idiomatique est un schéma zod, et la raison pour laquelle zod s'y prête si bien, c'est que le même schéma s'exécute dans le navigateur et sur le serveur. Définissez-le une seule fois :

// lib/email.ts
import { z } from "zod";

export const signupSchema = z.object({
  email: z.string().email("Enter a valid email address."),
});

Dans un Client Component, analysez la valeur avec ce schéma à la soumission, pour que l'utilisateur reçoive une erreur instantanée sans aller-retour. safeParse renvoie un résultat discriminé que vous pouvez lire sans try/catch :

"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>
  );
}

Le contrôle côté client vaut la peine — il évite une requête sur les fautes de frappe évidentes — mais il est tout sauf digne de confiance. Un utilisateur qui a l'onglet réseau ouvert peut envoyer une requête POST directement à /api/signup et le contourner entièrement, si bien que le serveur doit réexécuter exactement le même schéma avant de croire quoi que ce soit.

Couche 2 : le domaine peut-il recevoir du courrier ?#

C'est le premier contrôle que le navigateur ne peut pas faire et que zod ne fera pas : un domaine sans enregistrement MX ne peut accepter de courrier pour personne, si bien qu'une seule résolution DNS élimine les domaines morts, les noms d'entreprise mal orthographiés et les TLD inventés. Un Route Handler Next.js s'exécute par défaut sur Node, vous disposez donc de node:dns/promises sans la moindre dépendance :

// 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;
  }
}

Un contrôle réussi ici signifie « le domaine accepte le courrier », pas « cette boîte aux lettres existe » — mais un échec est décisif et gratuit, ce qui est exactement ce que l'on attend d'une couche bon marché.

Couche 3 : la boîte aux lettres existe-t-elle réellement ?#

Les couches 1 et 2 ne peuvent qu'écarter une adresse. Un domaine peut publier des enregistrements MX parfaits et n'avoir malgré tout aucune boîte aux lettres à l'adresse que vous détenez — noreply-9f2x@gmail.com est une syntaxe valide sur une route de messagerie active, et c'est pourtant une boîte aux lettres qui n'a jamais été créée. Confirmer une boîte aux lettres précise passe par la conversation de livraison SMTP : se connecter au serveur de messagerie, émettre RCPT TO, lire la réponse et se déconnecter avant d'envoyer quoi que ce soit.

Vous pourriez scripter cela depuis un runtime Node, mais vous ne devriez pas l'exécuter depuis une fonction Next.js. La plupart des hébergeurs cloud bloquent le port 25 sortant, la réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et les serveurs destinataires font du greylisting et limitent le débit des expéditeurs inconnus — si bien qu'un test qui fonctionne en local échoue silencieusement, ou vous fait blacklister, en production. Comment fonctionne la vérification d'e-mail parcourt le pipeline complet, domaines catch-all compris. C'est la couche qu'il vaut la peine de déléguer.

Le point de terminaison de vérification de Qualisend exécute tout le pipeline depuis une infrastructure à réputation gérée et renvoie un verdict. Appelez-le côté serveur avec fetch, en lisant la clé depuis une variable d'environnement pour qu'elle ne quitte jamais la machine :

// 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"
}

La réponse est une enveloppe { "result": { ... } }. Vous lisez result.status aux côtés d'un score, d'un reason et de sub_flags pour des traits comme les adresses de rôle ou jetables :

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

Consultez la référence de l'API pour la forme exacte des champs ; l'assistant ci-dessus n'a besoin que de status pour rejeter une adresse qui ne sera pas délivrée. Notez que la variable d'environnement est simplement QUALISEND_API_KEY, et non préfixée par NEXT_PUBLIC_ — c'est ce préfixe qui intègre une valeur dans le bundle client, et une clé de vérification dans le navigateur est une clé que n'importe qui peut lire et dépenser.

Composer les couches pour valider une adresse e-mail dans Next.js#

Assemblez maintenant les trois assistants dans un seul route.ts, le moins coûteux d'abord, pour que le crédit d'API ne soit dépensé que sur une adresse qui a déjà passé la syntaxe et le contrôle MX :

// 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 });
}

Cet ordonnancement est toute l'astuce : le schéma court-circuite les valeurs farfelues, le contrôle DNS local écarte gratuitement les domaines morts, et l'API n'est sollicitée que pour les adresses qui ont franchi les deux premières couches. Le handler ne bloque durement que undeliverable et laisse passer risky et unknown, si bien qu'une adresse limite ou une défaillance passagère de l'API ne refoule jamais un vrai client — vous pouvez brancher sur score ou sur une entrée de sub_flags plus loin dans le flux plutôt que de bloquer l'inscription.

Si vous préférez soumettre le formulaire sans écrire de code de fetch côté client, le corps identique fonctionne dans une Server Action. Renvoyez { error } au lieu de NextResponse et consommez-le avec 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 };
}

C'est la même structure à trois couches que vous retrouverez dans le guide Node.js — c'est le découpage en couches, pas le framework, qui rend la validation fiable. Exécutez ce pipeline rapide de façon synchrone à l'inscription et réservez les vérifications par lots plus poussées au nettoyage de listes ; le guide de l'inscription serverless montre le schéma en temps réel de bout en bout, et la comparaison d'API aligne les services capables de soutenir la troisième couche.

Foire aux questions#

z.string().email() suffit-il pour valider une adresse e-mail dans Next.js ?#

Pour la syntaxe, oui — z.string().email() est le bon contrôle de première couche et bien supérieur à une regex maison. Mais il valide la forme : il ne résout jamais le DNS et ne contacte jamais un serveur de messagerie, si bien qu'un contrôle réussi signifie « ressemble à un e-mail », pas « sera délivré ». Et un contrôle uniquement côté navigateur se contourne trivialement en envoyant une requête directement à votre route. Réexécutez le schéma sur le serveur, puis ajoutez une résolution MX et un test SMTP de boîte aux lettres avant de faire confiance à l'adresse.

Où placer la clé d'API Qualisend dans une application Next.js ?#

Dans une variable d'environnement réservée au serveur, nommée QUALISEND_API_KEY — jamais dans une variable préfixée par NEXT_PUBLIC_. Le préfixe NEXT_PUBLIC_ intègre une valeur dans le bundle client, si bien qu'une clé de vérification portant ce préfixe est lisible par toute personne qui ouvre votre site. Lisez process.env.QUALISEND_API_KEY à l'intérieur d'un Route Handler ou d'une Server Action, où elle reste sur le serveur, et laissez le navigateur appeler votre propre point de terminaison plutôt que Qualisend directement.

Faut-il valider un e-mail côté client ou côté serveur dans Next.js ?#

Les deux, mais seul le serveur fait foi. Exécutez le schéma zod partagé dans le Client Component pour un retour instantané sur les fautes de frappe, et réexécutez exactement le même schéma dans le Route Handler ou la Server Action, car le contrôle client peut être entièrement contourné. Les couches DNS et SMTP sont par nature réservées au serveur — elles nécessitent des API Node et une clé secrète — de sorte que le navigateur ne les voit jamais.

Puis-je exécuter la vérification DNS/MX sur le runtime Edge ?#

Non. node:dns/promises nécessite le runtime Node.js, celui qu'utilisent les Route Handlers par défaut ; une route avec export const runtime = "edge" n'a pas de module dns. Si vous avez besoin d'Edge, retirez le filtre MX local et laissez Qualisend résoudre le DNS dans le cadre de l'appel de vérification — son pipeline effectue déjà la résolution MX et le test SMTP, si bien que l'unique appel d'API couvre les couches deux et trois.


Prêt à ajouter la couche boîte aux lettres ? Déposez une adresse syntaxiquement parfaite dans le vérificateur d'e-mail gratuit pour voir une chaîne approuvée par zod revenir undeliverable, puis reliez le même verdict à votre Route Handler grâce aux exemples prêts à copier-coller de la référence de l'API.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started