Skip to content
Inizia con 100 crediti di verifica gratuiti
Qualisend
Tutti gli articoli
Ingegneria / 11 maggio 2026

Come validare un indirizzo email in Express

9 min di lettura

Qualisend team
Come validare un indirizzo email in Express

Per validare un indirizzo email in Express devi collegare un middleware, non un singolo controllo, e quel middleware deve rispondere a tre domande diverse, in ordine. L'indirizzo ha la forma corretta? Il suo dominio può ricevere posta? La casella esiste davvero? Express di suo non offre nessuna di queste verifiche, ma il suo ecosistema le riduce a poche righe ciascuna: express-validator per la sintassi, il modulo integrato di Node dns/promises per il dominio e un'API di verifica per la casella. Questa guida le costruisce tutte e tre come validatori su un'unica route POST e mostra esattamente dove finisce il framework e dove inizia un controllo vero. È la versione per Express della guida alla validazione email in Node.js, quindi il ragionamento su DNS e SMTP resta valido tale e quale.

In breve#

Concatena tre validatori sul campo email, partendo dal meno costoso, e lascia che .bail() di express-validator interrompa la catena appena uno è decisivo: usa body('email').isEmail() per la sintassi, un validatore personalizzato asincrono che incapsula resolveMx di dns/promises per il dominio e un secondo validatore personalizzato asincrono che chiama un'API di verifica per la casella, restituendo un 422 quando l'esito è undeliverable. Non provare ad aprire connessioni SMTP dal tuo processo Express per testare le caselle da solo: la porta 25 in uscita è bloccata sulla maggior parte degli host e la risposta dipende dalla reputazione dell'IP di invio e dal greylisting, cose che non vuoi reimplementare. Ogni livello esclude gli indirizzi a un costo minore del successivo; solo l'API può confermarne uno.

Livello 1: la sintassi con express-validator#

express-validator è di fatto il middleware di validazione standard per Express: un wrapper sottile, nativo per Express, attorno alla libreria validator.js. Il suo controllo isEmail() è l'isEmail di validator.js, quindi ottieni lo stesso parser di sintassi collaudato su cui già si appoggia ogni progetto Node, esposto come middleware concatenabile. Dichiara una regola sul campo e verrà eseguita prima del tuo handler:

import { body } from "express-validator";

const validateEmail = body("email")
  .trim()
  .isEmail()
  .withMessage("Enter a valid email address.")
  .bail();

.trim() elimina gli spazi iniziali e finali, .isEmail() valida la forma, .withMessage() imposta il testo dell'errore e .bail() ferma la catena per questo campo nel momento in cui la sintassi fallisce, così i livelli più costosi qui sotto non vengono mai eseguiti su una stringa malformata. Quel .bail() è l'equivalente Express del fermarsi al controllo meno costoso possibile.

Sappi esattamente cosa ottieni. isEmail() legge la stringa: non risolve mai il DNS e non apre mai un socket. definitely-fake@gmail.com passa. info@company-that-folded.com passa. typo@gmial.com passa. Tutti e tre sono non recapitabili e nessun validatore che si limita a esaminare la stringa te lo dirà mai: è lo stesso motivo per cui la validazione email con le regex fallisce. Sintassi e deliverability sono domande diverse: una riguarda la stringa, l'altra riguarda internet.

Livello 2: il dominio può ricevere posta?#

Questo è il primo livello che Express non ti offre, ed è poco costoso da aggiungere. Un dominio senza record MX non può ricevere posta per nessuno, quindi una sola ricerca DNS elimina domini morti, nomi di aziende scritti male e TLD inventati prima che tu spenda qualcosa in rete. Il modulo integrato di Node node:dns/promises risolve i record MX senza dipendenze e .custom() di express-validator accetta una funzione asincrona: solleva un errore al suo interno per far fallire il campo, restituisci true per farlo passare:

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

async function hasMailRoute(email) {
  const domain = email.slice(email.lastIndexOf("@") + 1);

  let records = [];
  try {
    records = await resolveMx(domain);
  } catch {
    // ENOTFOUND / ENODATA — the domain doesn't exist or publishes no MX
    throw new Error("This domain cannot receive email.");
  }

  if (records.length === 0) {
    throw new Error("This domain cannot receive email.");
  }
  return true;
}

Poiché è una semplice funzione asincrona, entra direttamente nella catena come validatore personalizzato, protetta da un altro .bail(), così il livello della casella viene eseguito solo su un dominio con un instradamento della posta che si risolve davvero:

body("email")
  .isEmail()
  .bail()
  .custom(hasMailRoute)
  .bail();
await hasMailRoute("jane@gmail.com");               // true
await hasMailRoute("jane@company-that-folded.com"); // throws — no MX

Alcuni domini accettano posta su un record A senza MX (MX implicito). Se vuoi gestire questo caso limite, ricorri a resolve4 quando resolveMx non restituisce nulla, ma per la stragrande maggioranza degli indirizzi reali un controllo MX è il filtro giusto.

Livello 3: la casella esiste davvero?#

I livelli 1 e 2 possono solo escludere un indirizzo. Un dominio può pubblicare record MX perfetti e non avere comunque una casella all'indirizzo che hai in mano: noreply-9f2x@gmail.com ha una sintassi valida su un instradamento della posta attivo, ma resta una casella mai creata. Confermare una casella specifica significa affrontare la conversazione SMTP di recapito: connettersi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di inviare qualsiasi cosa.

In teoria potresti implementarla da Express con il modulo net. In pratica non dovresti eseguirla dal tuo server applicativo: la maggior parte degli host cloud blocca la porta 25 in uscita, la risposta dipende dalla reputazione dell'IP da cui ti connetti e i server riceventi applicano greylisting e limiti di frequenza ai mittenti sconosciuti, quindi un test che funziona in locale fallisce silenziosamente in produzione, o ti fa finire in blacklist. Come funziona la verifica email illustra l'intera pipeline, domini catch-all compresi. Questo è il livello che conviene delegare.

Chiamare Qualisend da un validatore personalizzato asincrono#

La delega è solo un altro validatore personalizzato. L'endpoint di verifica di Qualisend esegue l'intera pipeline (sintassi, DNS e test SMTP della casella) da un'infrastruttura con reputazione gestita. Risponde subito, spesso prima che il test della casella sia terminato, e l'esito del test arriva poi su un job che puoi leggere in seguito. Chiamalo con il fetch globale (integrato in Node 18+), leggi la tua chiave da una variabile d'ambiente, limita l'attesa con un timeout e solleva un errore solo quando l'esito è undeliverable:

async function isDeliverable(email) {
  let res;
  try {
    res = await fetch("https://app.qualisend.com/api/v1/verify", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(5000),
    });
  } catch {
    // A timeout or a network error: don't block a real signup.
    return true;
  }

  // Out of credits, rate limited or our own outage: don't block a real signup.
  if (!res.ok) return true;

  const { job_id, result } = await res.json();

  if (result.status === "undeliverable") {
    throw new Error("We couldn't confirm a mailbox at this address.");
  }

  // The mailbox probe is still running: collect its verdict off the request path.
  if (result.smtp === "pending") {
    recheckLater(email, job_id).catch(() => {});
  }
  return true;
}

Il riferimento API documenta richiesta e risposta complete. L'esito torna in un envelope { "result": { ... } } accanto al job_id:

{
  "job_id": "8c2f4e1a-5b7d-4c3e-9a6f-2d1b0e7c4a91",
  "result": {
    "status": "unknown",
    "score": 60,
    "smtp": "pending",
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
  }
}

status è uno tra deliverable, risky, undeliverable e unknown. Il validatore qui sopra fa fallire solo undeliverable e lascia passare risky e unknown, così puoi decidere a valle cosa farne (filtrare in base a score, o ramificare la logica in base a una voce di sub_flags come disposable) invece di trasformare un indirizzo dubbio in un errore del modulo. Considera il JSON qui sopra come la forma, non come il contratto; l'elenco completo dei campi si trova nella documentazione per sviluppatori.

Quando smtp è "pending", come nell'esempio qui sopra, il test della casella è ancora in corso: reason resta null e l'esito finale arriva sul job. La chiamata recheckLater lo raccoglie fuori dal flusso della richiesta, interrogando GET /jobs/{job_id} ogni pochi secondi finché il job non si chiude. In produzione, passa l'id del job a una coda o a un worker in background invece di fare polling nel processo:

async function recheckLater(email, jobId) {
  const url = `https://app.qualisend.com/api/v1/jobs/${jobId}?include=results`;
  const headers = { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` };

  for (let attempt = 0; attempt < 180; attempt++) {
    await new Promise((resolve) => setTimeout(resolve, 5000));

    let job;
    try {
      const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
      if (!res.ok) continue;
      job = await res.json();
    } catch {
      continue; // a network blip: try again on the next tick
    }

    if (job.status === "canceled" || job.status === "failed") return;
    if (job.status === "completed") {
      const [final] = job.results ?? [];
      if (final?.status === "undeliverable") {
        await flagEmail(email); // your own follow-up, e.g. ask the user to fix it
      }
      return;
    }
  }
}

Mettere insieme i livelli per validare un indirizzo email in Express#

Componi tutti e tre su un'unica catena body("email"), partendo dal meno costoso, con .bail() tra l'uno e l'altro, così un errore interrompe la catena prima che parta il livello successivo. Il validatore dell'API scatta solo su un indirizzo che ha già superato la sintassi e il controllo MX:

import { body } from "express-validator";

const validateEmail = [
  body("email")
    .trim()
    .isEmail()
    .withMessage("Enter a valid email address.")
    .bail()
    .custom(hasMailRoute)
    .bail()
    .custom(isDeliverable),
];

Collega quell'array prima del route handler, poi leggi gli errori raccolti con validationResult. Quando la catena fallisce, rispondi con un 422 e l'array degli errori; altrimenti tutti i livelli sono stati superati e puoi salvare l'indirizzo in sicurezza:

import express from "express";
import { validationResult } from "express-validator";

const app = express();
app.use(express.json());

app.post("/signup", validateEmail, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }

  // Every layer passed — safe to create the account.
  const { email } = req.body;
  return res.status(201).json({ email });
});

Tutto il segreto sta in quest'ordine: isEmail() scarta la spazzatura prima che parta il tuo codice, il controllo MX locale esclude i domini morti a costo zero e l'API viene chiamata solo per gli indirizzi che hanno superato entrambi. Una validazione nell'ordine sbagliato, o che salta i livelli poco costosi, spende un credito API per ogni errore di battitura. È la struttura a livelli, non il framework, a rendere affidabile la validazione; la forma è la stessa della versione per Node.js, con la catena di middleware di Express al posto della pipeline.

C'è una decisione da prendere in anticipo: cosa succede quando fallisce la chiamata all'API stessa. Un timeout di rete o un errore sollevato da fetch dentro isDeliverable non deve lasciare a un vero cliente un 422 che non può risolvere. Il validatore qui sopra attenua già questo caso: restituisce true in caso di timeout, errore di rete o risposta non OK come 402, 429 o 5xx, trattando un servizio irraggiungibile come unknown anziché undeliverable, lasciando passare l'iscrizione e rimandando la riverifica. I livelli di sintassi e MX sono già stati eseguiti in locale, quindi stai allentando solo il livello che dipende dalla rete.

Esegui questa pipeline veloce e sincrona al punto di acquisizione e riserva la verifica in batch, più pesante, alla pulizia delle liste. La guida alle iscrizioni serverless mostra il pattern in tempo reale dall'inizio alla fine e, quando devi scegliere quale servizio usare per il livello tre, il confronto tra API passa in rassegna le opzioni.

Domande frequenti

01L'isEmail di express-validator basta per validare un indirizzo email?

Per la sintassi, sì: body("email").isEmail() incapsula l'isEmail di validator.js ed è il controllo giusto per il primo livello, molto meglio di una regex scritta a mano. Ma valida la forma, non la deliverability: non risolve mai il DNS e non contatta mai un server di posta, quindi un esito positivo significa "sembra un'email", non "verrà recapitata". Abbinalo a una ricerca MX e a un controllo della casella prima di fidarti dell'indirizzo.

02Come scrivo un validatore personalizzato asincrono in Express?

Passa una funzione asincrona a .custom() di express-validator. La funzione riceve il valore del campo; restituisci true (o risolvi la promise) per farlo passare e solleva un Error con throw (o rifiuta la promise) per farlo fallire: il messaggio dell'eccezione diventa l'errore di validazione. È così che le funzioni hasMailRoute e isDeliverable viste sopra entrano nella stessa catena body("email"). Metti un .bail() prima di ciascuna, così un livello fallito ferma la catena invece di eseguire il successivo.

03Posso controllare se una casella esiste in Express senza un'API?

In parte. node:dns/promises conferma che il dominio accetta posta, il che esclude gratis i domini morti e non richiede dipendenze. Confermare la casella significa affrontare una conversazione SMTP che puoi tentare con il modulo net, ma che non dovresti eseguire dal tuo server applicativo: la porta 25 è ampiamente bloccata e il risultato dipende dalla reputazione del tuo IP. Un servizio di verifica esiste proprio per gestire questo livello; cos'è la verifica email spiega i termini.

04Devo restituire un 422 o lasciar passare l'iscrizione con un risultato sconosciuto?

Rifiuta undeliverable con un 422: è un indirizzo confermato come errato, che l'utente può correggere. Ma non far fallire nulla per un'interruzione del tuo servizio di verifica: considera una risposta non OK, un timeout o uno status unknown come "lascia passare e ricontrolla più tardi", così un problema di rete temporaneo non blocca mai una vera registrazione. Il validatore isDeliverable fa esattamente questo, restituendo true quando la risposta non è OK.


Vuoi aggiungere il livello della casella? Inserisci un indirizzo sintatticamente perfetto nella verifica email gratuita per vedere una stringa approvata da isEmail tornare undeliverable, poi collega lo stesso esito al tuo middleware Express con gli esempi da copiare e incollare nel riferimento API.

La tua reputazione, protetta.

Pulisci la tua prima lista in pochi minuti. 100 crediti gratuiti, senza carta di credito.

Inizia