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.



