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

Comment valider une adresse e-mail en Node.js

6 minutes read

Qualisend team
Une fenêtre de code Node.js validant un e-mail à travers les couches syntaxe, DNS et SMTP

Valider une adresse e-mail en Node.js, c'est trois métiers qui portent un seul nom. La plupart des tutoriels vous montrent une regex et s'arrêtent là : ils vérifient l'orthographe et appellent ça de la validation. La vraie validation est en couches : un contrôle de syntaxe peu coûteux, une résolution DNS et un test SMTP de la boîte aux lettres — et Node vous fournit les deux premières dès le départ. Ce guide construit chaque couche avec du code fonctionnel et montre où une API de vérification prend le relais.

La réponse courte#

Utilisez une regex permissive pour la syntaxe, node:dns/promises pour la résolution MX et une API de vérification pour le contrôle SMTP de la boîte aux lettres — dans cet ordre, du moins coûteux d'abord, en s'arrêtant dès qu'une couche est décisive. N'essayez pas d'ouvrir des connexions SMTP depuis le serveur de votre application : le port 25 est bloqué chez la plupart des hébergeurs, et même là où il ne l'est pas, le contrôle dépend de la réputation de l'IP d'envoi et d'une gestion du greylisting que vous n'avez pas envie de construire.

Couche 1 : la syntaxe#

La regex a sa place ici et nulle part ailleurs. Gardez-la permissive — l'objectif est d'attraper les fautes de frappe dès la saisie, pas de réimplémenter la RFC 5322 (qui n'aide de toute façon en rien) :

const SYNTAX = /^[^\s@"]+(?:\.[^\s@"]+)*@[^\s@.]+(?:\.[^\s@.]+)+$/;

export function isValidSyntax(email) {
  return typeof email === "string" && email.length <= 320 && SYNTAX.test(email);
}

Un succès ici signifie « ça vaut la peine de vérifier », pas « valide ». Chaque couche suivante suppose que la syntaxe est déjà correcte.

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

C'est ici que Node justifie sa présence. Le module intégré node:dns/promises résout les enregistrements MX sans aucune dépendance, et un domaine sans route de courrier ne peut accepter de messages pour personne — cette unique résolution élimine donc les domaines morts, les noms d'entreprise mal orthographiés et les TLD inventés :

import { resolveMx } from "node:dns/promises";

export async function hasMailRoute(domain) {
  try {
    const mx = await resolveMx(domain);
    return mx.length > 0;
  } catch {
    // ENOTFOUND / ENODATA → domain doesn't exist or publishes no MX
    return false;
  }
}
await hasMailRoute("gmail.com");            // true
await hasMailRoute("company-that-folded.com"); // false

Certains domaines acceptent le courrier via un enregistrement A sans MX (MX implicite). Si vous voulez tenir compte de ce cas particulier, repliez-vous sur dns.resolve4 lorsque resolveMx est vide — mais pour l'écrasante majorité des adresses réelles, un contrôle MX est le bon filtre.

Couche 3 : la boîte aux lettres existe-t-elle vraiment ?#

Les couches 1 et 2 ne peuvent qu'écarter une adresse. Confirmer une boîte aux lettres implique la conversation de livraison SMTP — RCPT TO, lire la réponse, se déconnecter avant d'envoyer quoi que ce soit. En principe, vous pouvez le faire avec le module net. En pratique, vous ne devriez pas : la plupart des fournisseurs 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 appliquent greylisting et limitation de débit aux expéditeurs inconnus. C'est la couche qu'il vaut la peine de déléguer.

Le POST /verify de Qualisend exécute le pipeline complet et renvoie un verdict. Les contrôles locaux reviennent immédiatement, le test SMTP étant mis en file d'attente :

const BASE = "https://app.qualisend.com/api/v1";

export async function verify(email) {
  const res = await fetch(`${BASE}/verify`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
  });
  if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
  return res.json();
}
{
  "job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
  "probe_queued": true,
  "result": {
    "email": "jane@example.com",
    "status": "deliverable",
    "score": 95,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false },
    "did_you_mean": null,
    "smtp": "pending",
    "reason": null
  }
}

Pour une validation d'inscription en direct, le result immédiat suffit généralement pour agir — rejeter undeliverable, proposer la correction did_you_mean, signaler disposable. Lorsque vous avez besoin du verdict confirmé par SMTP, interrogez le job jusqu'à ce que le test se termine :

export async function verifyAndWait(email, { tries = 10, delayMs = 1500 } = {}) {
  const { job_id } = await verify(email);
  for (let i = 0; i < tries; i++) {
    const res = await fetch(`${BASE}/jobs/${job_id}?include=results`, {
      headers: { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` },
    });
    const job = await res.json();
    if (job.status === "completed") return job.results[0]; // { status, score, reason, ... }
    await new Promise((r) => setTimeout(r, delayMs));
  }
  return null; // still processing — treat as unknown, retry later
}

Deux détails font trébucher le fetch naïf ci-dessus dès qu'il rencontre du trafic réel. D'abord, un service en amont lent ne doit pas figer votre processus Node — enveloppez la requête dans un délai d'expiration AbortController pour qu'elle échoue vite au lieu de bloquer la boucle d'événements. Ensuite, un 429 (débit limité) ou un 5xx est transitoire et mérite un nouvel essai, tandis qu'un 4xx comme 422 signifie que la charge utile est incorrecte et qu'un nouvel essai n'aidera pas — séparez donc les deux :

export async function verify(email, { timeoutMs = 4000 } = {}) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  try {
    const res = await fetch(`${BASE}/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: ctrl.signal,
    });
    if (res.status === 429 || res.status >= 500) {
      throw Object.assign(new Error(`retryable ${res.status}`), { retryable: true });
    }
    if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
    return res.json();
  } finally {
    clearTimeout(timer);
  }
}

Gardez la clé d'API dans process.env, jamais dans le bundle — cet appel a sa place du côté serveur de votre application Node, pas dans quoi que ce soit que vous livrez au navigateur.

Pour le nettoyage de listes, vous vérifiez des milliers d'adresses, pas une à la fois. Ne bouclez pas avec await (séquentiel et lent) et ne les lancez pas toutes d'un coup (vous déclencherez la limite de débit). Envoyez un nombre borné de requêtes concurrentes avec Promise.allSettled afin qu'une adresse rejetée ne coule pas tout le lot :

export async function verifyMany(emails, { concurrency = 5 } = {}) {
  const out = [];
  for (let i = 0; i < emails.length; i += concurrency) {
    const slice = emails.slice(i, i + concurrency);
    const settled = await Promise.allSettled(slice.map((e) => verify(e)));
    out.push(...settled); // each: { status: "fulfilled", value } | { status: "rejected", reason }
  }
  return out;
}

Rassemblez les entrées rejected, temporisez et ne réessayez que celles-là — les verdicts fulfilled sont déjà acquis. Ce même schéma à débit maîtrisé vous maintient sous la limite de débit du forfait sans la moindre dépendance supplémentaire.

Assembler les couches#

Le moins coûteux d'abord, on s'arrête dès qu'on a une réponse :

export async function validateEmail(email) {
  if (!isValidSyntax(email)) return { status: "undeliverable", reason: "invalid_email" };
  const domain = email.slice(email.lastIndexOf("@") + 1);
  if (!(await hasMailRoute(domain))) return { status: "undeliverable", reason: "invalid_domain" };
  const { result } = await verify(email);
  return result; // deliverable | risky | undeliverable | unknown, with reason + sub_flags
}

Les deux couches locales ne coûtent rien et attrapent instantanément la plupart des déchets ; la couche API ne s'exécute que sur les adresses qui valent l'aller-retour réseau. Cet ordonnancement est toute l'astuce — voyez comment fonctionne la vérification d'e-mails pour comprendre pourquoi chaque étape se trouve là où elle est.

Foire aux questions#

Une bibliothèque comme validator.js ou email-validator suffit-elle ?#

Ces bibliothèques traitent bien la couche 1 — la syntaxe — et remplacent avantageusement une regex bricolée à la main. Mais ce sont des contrôleurs de syntaxe : elles ne résolvent pas les enregistrements MX et ne confirment pas l'existence d'une boîte aux lettres. Un validator.isEmail() qui passe signifie donc toujours « on dirait un e-mail », pas « le message sera délivré ». Associez-les aux couches DNS et SMTP ci-dessus.

Puis-je vérifier depuis Node l'existence d'un e-mail sans API ?#

En partie. node:dns/promises confirme que le domaine accepte le courrier, ce qui écarte gratuitement les domaines morts. Confirmer la boîte aux lettres suppose un test SMTP, que vous pouvez tenter avec le module net mais que vous ne devriez pas exécuter depuis le serveur de votre application — le port 25 est largement bloqué et le résultat dépend de la réputation de votre IP. C'est la couche qu'un service de vérification existe pour prendre en charge.

Faut-il valider les e-mails de façon synchrone à l'inscription ?#

Exécutez les couches instantanées de façon synchrone — la syntaxe et le MX sont assez rapides pour bloquer la requête et donner à l'utilisateur un retour immédiat. Traitez le résultat SMTP comme la réponse plus lente : agissez sur le verdict local immédiat à l'inscription et servez-vous du résultat complet confirmé par SMTP pour le nettoyage de listes. Le guide d'inscription serverless montre le schéma de bout en bout.

Faut-il mettre en cache les résultats de vérification pour éviter de recontrôler la même adresse ?#

Oui. Chaque appel de vérification est un aller-retour réseau et un crédit consommé : ne le répétez pas pour une adresse que vous venez de contrôler. Indexez un cache à courte durée de vie sur l'e-mail normalisé — en minuscules et sans espaces superflus au préalable — et réutilisez le verdict pendant quelques minutes. Une réserve : ne mettez pas en cache les résultats unknown ou encore en pending, car ce sont justement ceux qu'il vaut la peine de réessayer une fois le test SMTP terminé. Une simple Map suffit pour un unique processus Node ; ne recourez à Redis que lorsque vous exécutez plusieurs instances qui doivent partager le cache.


Prêt à ajouter la couche SMTP ? Le forfait gratuit inclut 100 crédits qui exécutent le pipeline complet, et la référence de l'API détaille l'ensemble des endpoints /verify et /jobs avec des exemples prêts à copier-coller dans sept langages.

Your reputation, protected.

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

Get started