Vérification d'e-mails dans Vue 3
Ce tutoriel construit un composable useEmailVerification réutilisable pour la Composition API de Vue 3. Il donne un retour immédiat à l'utilisateur pendant la saisie — valide, invalide, ou « vouliez-vous dire gmail.com ? » — tout en gardant votre clé d'API secrète bien au chaud sur le serveur.
Ce que vous allez construire
- Un petit endpoint serveur qui relaie la vérification (pour que la clé d'API n'atteigne jamais le navigateur)
- Un composable
useEmailVerificationtemporisé qui renvoie un état réactif - Un formulaire d'inscription avec état en ligne, suggestion de correction et revérification à l'envoi
Prérequis
- Vue 3 (Vite ou Nuxt) avec TypeScript
- Une route back-end que vous contrôlez (Node, Python, etc.) pour le relais
- Une clé d'API Mailbeam (inscription gratuite)
Étape 1 — Ajouter un endpoint de vérification côté serveur
N'appelez jamais Mailbeam directement depuis le navigateur : cela exposerait votre clé. Ajoutez un relais léger sur votre back-end. Le voici en Node/Express :
// server/routes/verify-email.js
// Pas encore de SDK : voici le wrapper de 12 lignes 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: "email required" });
try {
const { valid, score, reason, didYouMean } = await verifyEmail(email);
res.json({ acceptable: valid && score >= 60, reason, didYouMean });
} catch (err) {
// Échec permissif : ne bloquez pas l'interface sur une erreur d'API
res.json({ acceptable: true, reason: null, didYouMean: null });
}
}Étape 2 — Créer le 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"; // échec permissif dans l'interface
reason.value = null;
}
}
// Temporisation : on attend 500 ms après la dernière frappe
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 };
}Étape 3 — Le brancher sur un formulaire
<!-- 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() {
// Revérification côté serveur à l'envoi — ne faites jamais confiance au seul état client
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) {
// remonter l'erreur de validation du serveur
}
}
function applySuggestion() {
if (suggestion.value) email.value = suggestion.value;
}
</script>
<template>
<form @submit.prevent="onSubmit">
<label for="email">Adresse e-mail</label>
<input id="email" v-model="email" type="email" autocomplete="email" />
<p v-if="status === 'checking'" class="hint">Vérification…</p>
<p v-else-if="status === 'valid'" class="hint ok">✓ Adresse correcte</p>
<p v-else-if="status === 'invalid'" class="hint err">
Cette adresse semble non distribuable ({{ reason }}).
</p>
<p v-if="suggestion" class="hint">
Vouliez-vous dire
<button type="button" @click="applySuggestion">{{ suggestion }}</button> ?
</p>
<input v-model="password" type="password" autocomplete="new-password" />
<button type="submit" :disabled="!canSubmit">Créer le compte</button>
</form>
</template>Étape 4 — Revérifier systématiquement sur le serveur
Le composable améliore l'expérience, mais l'état client peut être falsifié ou contourné. Votre handler /api/signup doit refaire la même vérification avant de créer l'utilisateur — réutilisez la logique de relais de l'étape 1.
Bonnes pratiques
| Pratique | Pourquoi |
|---|---|
| Temporiser à ~500 ms | Un appel par pause, pas un par touche |
| Annuler les requêtes en vol | Évite les résultats hors séquence pendant la saisie |
| Passer par un relais sur votre serveur | Garde la clé d'API hors du client |
| Revérifier à l'envoi | L'état client est indicatif, pas faisant foi |
Exposer didYouMean | Rattrape les fautes de frappe du type gmial.com |
Liste de contrôle avant la mise en production
- L'endpoint de vérification est limité en débit par IP
-
MAILBEAM_KEYn'existe que côté serveur - Le handler d'envoi revérifie côté serveur
- L'interface échoue en mode permissif si le contrôle plante
- Les états de chargement et d'erreur sont accessibles (aria-live)