Verificación de email en Node.js
En este tutorial añadirás verificación de email en tiempo real a una aplicación Node.js con Express. Al terminar, los emails desechables, los buzones inexistentes y las direcciones con erratas se rechazarán en el endpoint de registro antes de crear ningún usuario.
Qué vas a construir
Un middleware de Express reutilizable que:
- Verifica una dirección de email con la API de Mailbeam
- Devuelve un 422 con un código de error legible por máquina cuando falla
- Falla en abierto ante errores de la API (para que una caída de Mailbeam no rompa tu registro)
- Opcionalmente cachea los resultados para no verificar dos veces lo mismo
Requisitos previos
- Node.js 18 o posterior
- Un proyecto con npm, pnpm o yarn
- Una cuenta de Mailbeam y una clave de API (regístrate gratis)
Paso 1 — Añade un cliente pequeño
No hay ningún SDK que instalar. Mailbeam es un único endpoint HTTP, así que con una docena de líneas en tu propio código lo cubres, y no hay nada que mantener actualizado. Los SDK oficiales están en la hoja de ruta; esto es lo que hay que hacer hoy.
// lib/mailbeam.js
const ENDPOINT = "https://api.mailbeam.dev/v1/verify";
export async function verifyEmail(email, { timeoutMs = 3000 } = {}) {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILBEAM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
const { error, message } = await response.json();
throw Object.assign(new Error(message), { code: error });
}
return response.json();
}Paso 2 — Configura tu clave de API
Añade tu clave a .env o a .env.local:
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxxPaso 3 — Crea el middleware de verificación
Crea middleware/verifyEmail.js:
import { verifyEmail } from "../lib/mailbeam.js";
/**
* Middleware de Express que verifica el email de req.body.email.
* Llama a next() si va bien, devuelve 422 si falla.
* Falla en abierto ante errores de la API de Mailbeam: no bloquees a los
* usuarios si la API está caída.
*/
export async function verifyEmail(req, res, next) {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: "El campo email es obligatorio." });
}
try {
const { valid, score, reason } = await verifyEmail(email);
if (!valid || score < 60) {
return res.status(422).json({
error: "Escribe una dirección de email válida.",
code: reason ?? "invalid_email",
});
}
// Adjunta el resultado a la petición para que lo usen los handlers de después
req.emailVerification = { valid, score, reason };
next();
} catch (err) {
// Registra el error pero no bloquees: una caída de Mailbeam no debería
// romper tu registro
console.error("[Mailbeam] error de verificación:", err.message);
next();
}
}Paso 4 — Añádelo a tu ruta de registro
// routes/auth.js
import express from "express";
import { verifyEmail } from "../middleware/verifyEmail.js";
const router = express.Router();
router.post("/signup", verifyEmail, async (req, res) => {
const { email, password } = req.body;
try {
const user = await db.createUser({ email, password });
res.status(201).json({ user });
} catch (err) {
res.status(500).json({ error: "No se ha podido crear la cuenta." });
}
});
export default router;Y la configuración de tu app de Express:
// app.js
import express from "express";
import authRoutes from "./routes/auth.js";
const app = express();
app.use(express.json());
app.use("/api/auth", authRoutes);
app.listen(3000, () => {
console.log("Servidor en marcha en http://localhost:3000");
});Probar la integración
Arranca tu servidor y prueba con cURL:
# Email válido — debería devolver 201
curl -X POST http://localhost:3000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"email": "valid@example.com", "password": "secretpass"}'
# Email desechable — debería devolver 422
curl -X POST http://localhost:3000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"email": "throwaway@mailinator.com", "password": "secretpass"}'Usa los dominios de prueba de Mailbeam en tu pipeline de CI para no consumir cuota:
# Siempre devuelve válido (score 99)
curl ... -d '{"email": "user@valid.mailbeam-test.dev", ...}'
# Siempre devuelve inválido
curl ... -d '{"email": "user@invalid.mailbeam-test.dev", ...}'Añadir una capa de caché (opcional)
En registros con mucho tráfico, cachea los resultados de verificación para evitar llamadas redundantes:
import { LRUCache } from "lru-cache";
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 60, // 1 hora
});
export async function verifyEmail(req, res, next) {
const { email } = req.body;
if (!email) return res.status(400).json({ error: "El campo email es obligatorio." });
const normalised = email.toLowerCase().trim();
const cached = cache.get(normalised);
if (cached) {
if (!cached.valid || cached.score < 60) {
return res.status(422).json({
error: "Escribe una dirección de email válida.",
code: cached.reason ?? "invalid_email",
});
}
return next();
}
try {
const result = await verifyEmail(normalised);
cache.set(normalised, result);
if (!result.valid || result.score < 60) {
return res.status(422).json({
error: "Escribe una dirección de email válida.",
code: result.reason ?? "invalid_email",
});
}
next();
} catch (err) {
console.error("[Mailbeam] error de verificación:", err.message);
next();
}
}Buenas prácticas
| Práctica | Por qué |
|---|---|
| Fallar en abierto ante errores de la API | Una caída de Mailbeam no debería impedir los registros |
| Cachear los resultados (TTL de 1 h) | Evita verificar dos veces la misma dirección |
| Pasar a minúsculas y recortar antes de verificar | Evita fallos de caché por diferencias triviales |
| Usar dominios de prueba en CI | No consumas cuota de producción en los tests automáticos |
| Registrar errores, no emails | Evita escribir datos personales en los logs de producción |
Checklist de producción
-
MAILBEAM_KEYdefinida como secreto de la plataforma (no en el código) - Registro de errores configurado (sin escribir la dirección de email)
- Capa de caché añadida en las rutas con mucho tráfico
- Dominios de prueba usados en el pipeline de CI
- Umbral de puntuación decidido y documentado (por defecto: 60)
- El frontend muestra un mensaje de error claro cuando llega un 422