Verificación de email en Vue 3
Este tutorial construye un composable reutilizable useEmailVerification para la Composition API de Vue 3. Da feedback instantáneo mientras la persona escribe —válido, inválido o «¿querías decir gmail.com?»— manteniendo tu clave secreta de API a salvo en el servidor.
Qué vas a construir
- Un endpoint pequeño en el servidor que hace de proxy de la verificación (para que la clave nunca llegue al navegador)
- Un composable
useEmailVerificationcon debounce que devuelve estado reactivo - Un formulario de registro con estado en línea, sugerencias de erratas y una reverificación al enviar
Requisitos previos
- Vue 3 (Vite o Nuxt) con TypeScript
- Una ruta de backend bajo tu control (Node, Python, etc.) para el proxy
- Una clave de API de Mailbeam (regístrate gratis)
Paso 1 — Añade un endpoint de verificación en el servidor
Nunca llames a Mailbeam directamente desde el navegador: expondrías tu clave. Añade un proxy fino en tu backend. Aquí está en Node/Express:
// server/routes/verify-email.js
// Todavía no hay SDK: este es el envoltorio de 12 líneas de /docs/quickstart.
import { verifyEmail } from "./lib/mailbeam";
export async function verifyEmail(req, res) {
const { email } = req.body;
if (!email) return res.status(400).json({ error: "falta el email" });
try {
const { valid, score, reason, didYouMean } = await verifyEmail(email);
res.json({ acceptable: valid && score >= 60, reason, didYouMean });
} catch (err) {
// Falla en abierto: no bloquees la interfaz por un error de la API
res.json({ acceptable: true, reason: null, didYouMean: null });
}
}Paso 2 — Crea el composable
// src/composables/useEmailVerification.ts
import { ref, watch } from "vue";
type Status = "idle" | "checking" | "valid" | "invalid" | "error";
export function useEmailVerification(emailRef: import("vue").Ref<string>) {
const status = ref<Status>("idle");
const reason = ref<string | null>(null);
const suggestion = ref<string | null>(null);
let controller: AbortController | null = null;
let timer: ReturnType<typeof setTimeout> | null = null;
async function check(email: string) {
controller?.abort();
controller = new AbortController();
status.value = "checking";
try {
const res = await fetch("/api/verify-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: controller.signal,
});
const data = await res.json();
suggestion.value = data.didYouMean ?? null;
if (data.acceptable) {
status.value = "valid";
reason.value = null;
} else {
status.value = "invalid";
reason.value = data.reason ?? "invalid_email";
}
} catch (err) {
if ((err as Error).name === "AbortError") return;
status.value = "error"; // falla en abierto en la interfaz
reason.value = null;
}
}
// Debounce: espera 500 ms tras dejar de escribir
watch(emailRef, (email) => {
if (timer) clearTimeout(timer);
suggestion.value = null;
if (!email || !email.includes("@")) {
status.value = "idle";
return;
}
timer = setTimeout(() => check(email), 500);
});
return { status, reason, suggestion };
}Paso 3 — Conéctalo a un formulario
<!-- src/components/SignupForm.vue -->
<script setup lang="ts">
import { ref, computed } from "vue";
import { useEmailVerification } from "@/composables/useEmailVerification";
const email = ref("");
const password = ref("");
const { status, reason, suggestion } = useEmailVerification(email);
const canSubmit = computed(
() => status.value === "valid" && password.value.length >= 8
);
async function onSubmit() {
// Reverifica en el servidor al enviar: nunca te fíes solo del estado del cliente
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.value, password: password.value }),
});
if (!res.ok) {
// muestra el error de validación del servidor
}
}
function applySuggestion() {
if (suggestion.value) email.value = suggestion.value;
}
</script>
<template>
<form @submit.prevent="onSubmit">
<label for="email">Email</label>
<input id="email" v-model="email" type="email" autocomplete="email" />
<p v-if="status === 'checking'" class="hint">Comprobando…</p>
<p v-else-if="status === 'valid'" class="hint ok">✓ Tiene buena pinta</p>
<p v-else-if="status === 'invalid'" class="hint err">
Este email parece no entregable ({{ reason }}).
</p>
<p v-if="suggestion" class="hint">
¿Querías decir
<button type="button" @click="applySuggestion">{{ suggestion }}</button>?
</p>
<input v-model="password" type="password" autocomplete="new-password" />
<button type="submit" :disabled="!canSubmit">Crear cuenta</button>
</form>
</template>Paso 4 — Verifica siempre también en el servidor
El composable mejora la experiencia, pero el estado del cliente se puede falsear o saltar. Tu handler de /api/signup tiene que ejecutar la misma verificación antes de crear el usuario: reutiliza la lógica del proxy del paso 1.
Buenas prácticas
| Práctica | Por qué |
|---|---|
| Debounce de ~500 ms | Una llamada por pausa, no por pulsación |
| Abortar las peticiones en vuelo | Evita resultados desordenados mientras se escribe |
| Pasar por el proxy de tu servidor | Mantiene la clave de API fuera del cliente |
| Reverificar al enviar | El estado del cliente es orientativo, no autoritativo |
Mostrar didYouMean | Rescata a quien escribe gmial.com |
Checklist de producción
- Endpoint de verificación con límite de tasa por IP
-
MAILBEAM_KEYsolo en el servidor - El handler de envío reverifica en el servidor
- La interfaz falla en abierto si la comprobación da error
- Los estados de carga y de error son accesibles (aria-live)