Ce que vous allez construire
À la fin de ce guide, votre formulaire React :
- Valide les emails en temps réel via l’API Syvel
- Affiche un indicateur visuel (loading → ✓ valide / ✗ problème)
- Ne bloque la soumission que pour les adresses clairement problématiques (
is_risky === true) - Affiche un avertissement optionnel pour les adresses suspectes (score 50–64)
- Propose une correction de typo si
did_you_meanest retourné (gmial.com→gmail.com) - Respecte les bonnes pratiques UX : pas de validation intrusive, debounce, gestion d’erreur gracieuse
Le tout en moins de 60 lignes de code.
Prérequis
- Projet React avec React Hook Form (
npm install react-hook-form) - Une clé API Syvel (gratuit, créez un compte →) — consultez nos tarifs si vous avez besoin de volumes importants
- Node 18+
Réponse API : les champs utilisés
L’endpoint GET /v1/check/{email} retourne :
interface SyvelResult { risk_score: number; // 0-100 is_risky: boolean; // true si risk_score >= seuil projet (défaut: 65) reason: 'safe' | 'disposable' | 'undeliverable' | 'role_account'; did_you_mean: string | null; // ex: "gmail.com" pour "gmial.com" deliverability_score: number; // 0-100 is_free_provider: boolean; is_corporate_email: boolean; is_alias_email: boolean; mx_provider_label: string | null;}Étape 1 : Créer le hook de validation
Créez un fichier src/hooks/useEmailValidation.ts :
import { useState, useCallback } from 'react';
interface SyvelResult { risk_score: number; is_risky: boolean; reason: 'safe' | 'disposable' | 'undeliverable' | 'role_account'; did_you_mean: string | null; deliverability_score: number;}
interface ValidationState { status: 'idle' | 'loading' | 'valid' | 'warning' | 'error'; message?: string; didYouMean?: string; score?: number;}
export function useEmailValidation(apiKey: string) { const [state, setState] = useState<ValidationState>({ status: 'idle' });
const validate = useCallback(async (email: string): Promise<string | true> => { if (!email || email.length < 5) return true; // Laisser passer, validation format s'en charge
setState({ status: 'loading' });
try { const res = await fetch( `https://api.syvel.io/v1/check/${encodeURIComponent(email)}`, { headers: { 'Authorization': `Bearer ${apiKey}` } } );
if (!res.ok) { // En cas d'erreur API, ne pas bloquer l'utilisateur (fail open) setState({ status: 'idle' }); return true; }
const data: SyvelResult = await res.json();
// Typo détectée — avertir sans bloquer if (data.did_you_mean) { setState({ status: 'warning', message: `Vouliez-vous dire ${data.did_you_mean} ?`, didYouMean: data.did_you_mean, score: data.risk_score, }); // On ne bloque pas, mais on avertit return true; }
if (data.is_risky) { let message = 'Cette adresse email n\'est pas acceptée.'; if (data.reason === 'undeliverable') { message = 'Ce domaine ne peut pas recevoir d\'emails.'; } else if (data.reason === 'role_account') { message = 'Veuillez utiliser une adresse email personnelle.'; } setState({ status: 'error', message, score: data.risk_score }); return message; }
// Score modéré (non bloquant) : avertissement doux if (data.risk_score >= 50) { setState({ status: 'warning', message: 'Cette adresse semble temporaire. Utilisez votre email professionnel.', score: data.risk_score, }); return true; }
setState({ status: 'valid', score: data.risk_score }); return true;
} catch { // Erreur réseau : fail open (ne jamais bloquer un utilisateur légitime) setState({ status: 'idle' }); return true; } }, [apiKey]);
return { state, validate };}Points importants :
- Fail open : si l’API Syvel est inaccessible, on laisse passer l’email. Ne jamais bloquer un utilisateur légitime à cause d’une erreur serveur externe.
is_risky: champ de blocage recommandé. Le seuil est configurable dans votre dashboard.did_you_mean: proposez la correction en temps réel plutôt que de bloquer.- Score 50–64 : avertissement visible mais soumission autorisée. Utile pour les formulaires B2C.
Étape 2 : Créer le composant d’indicateur
Créez src/components/EmailInput.tsx :
import React from 'react';import { UseFormRegister, FieldErrors } from 'react-hook-form';
interface ValidationState { status: 'idle' | 'loading' | 'valid' | 'warning' | 'error'; message?: string; didYouMean?: string;}
interface Props { register: UseFormRegister<any>; errors: FieldErrors; validationState: ValidationState; onDidYouMeanClick?: (suggestion: string) => void; name?: string;}
function StatusIcon({ status }: { status: ValidationState['status'] }) { switch (status) { case 'loading': return <span className="email-icon loading" aria-hidden>⟳</span>; case 'valid': return <span className="email-icon valid" aria-hidden>✓</span>; case 'warning': return <span className="email-icon warning" aria-hidden>⚠</span>; case 'error': return <span className="email-icon error" aria-hidden>✗</span>; default: return null; }}
export function EmailInput({ register, errors, validationState, onDidYouMeanClick, name = 'email' }: Props) { return ( <div className="field-wrapper"> <label htmlFor={name}>Adresse email</label> <div className="input-wrapper"> <input id={name} type="email" autoComplete="email" aria-invalid={!!errors[name]} aria-describedby={`${name}-hint`} className={`email-input status-${validationState.status}`} {...register(name)} /> <StatusIcon status={validationState.status} /> </div>
{/* Message d'erreur RHF (format invalide) */} {errors[name] && ( <p id={`${name}-hint`} role="alert" className="field-error"> {errors[name]?.message as string} </p> )}
{/* Avertissement Syvel (non-bloquant) */} {validationState.status === 'warning' && !errors[name] && ( <p id={`${name}-hint`} role="status" className="field-warning"> {validationState.message} {validationState.didYouMean && onDidYouMeanClick && ( <button type="button" className="did-you-mean-btn" onClick={() => onDidYouMeanClick(validationState.didYouMean!)} > Utiliser {validationState.didYouMean} </button> )} </p> )} </div> );}Étape 3 : Assembler le formulaire
import React from 'react';import { useForm } from 'react-hook-form';import { useEmailValidation } from '../hooks/useEmailValidation';import { EmailInput } from '../components/EmailInput';
const API_KEY = import.meta.env.VITE_SYVEL_API_KEY;
interface FormData { email: string; name: string;}
export function SignupForm() { const { state: emailState, validate } = useEmailValidation(API_KEY);
const { register, handleSubmit, setValue, formState: { errors, isSubmitting }, } = useForm<FormData>({ mode: 'onBlur', // Valider à la perte de focus, pas à chaque frappe });
const handleDidYouMean = (suggestion: string) => { // Remplace le domaine dans le champ email par la suggestion const currentEmail = (document.getElementById('email') as HTMLInputElement)?.value; if (currentEmail && currentEmail.includes('@')) { const local = currentEmail.split('@')[0]; setValue('email', `${local}@${suggestion}`, { shouldValidate: true }); } };
const onSubmit = async (data: FormData) => { console.log('Formulaire soumis :', data); // Votre logique d'inscription ici };
return ( <form onSubmit={handleSubmit(onSubmit)} noValidate> <div className="field-wrapper"> <label htmlFor="name">Nom</label> <input id="name" type="text" autoComplete="name" {...register('name', { required: 'Le nom est requis.' })} /> {errors.name && <p className="field-error">{errors.name.message}</p>} </div>
<EmailInput register={register} errors={errors} validationState={emailState} onDidYouMeanClick={handleDidYouMean} name="email" />
<button type="submit" disabled={isSubmitting || emailState.status === 'loading'} > {isSubmitting ? 'Création en cours...' : 'Créer mon compte'} </button> </form> );}Pour brancher la validation Syvel au cycle React Hook Form :
// Dans le composant EmailInput, modifiez le register('email') ainsi :{...register(name, { required: 'L\'email est requis.', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Format d\'email invalide.', }, validate: async (value) => await validate(value),})}Étape 4 : Ajouter le debounce
Si vous validez en mode onChange plutôt que onBlur, ajoutez un debounce pour éviter un appel API à chaque frappe :
import { useMemo } from 'react';import { debounce } from 'lodash-es'; // ou votre propre implémentation
// Dans useEmailValidation, wrapper validate avec debounce :const debouncedValidate = useMemo( () => debounce(validate, 500), [validate]);En mode onBlur (recommandé), le debounce n’est pas nécessaire — la validation se déclenche uniquement quand l’utilisateur quitte le champ.
Variables d’environnement
Dans votre .env.local :
VITE_SYVEL_API_KEY=sv_votre_cle_iciNe jamais committer votre clé API. Ajoutez .env.local à votre .gitignore. En production, injectez la variable via votre pipeline CI/CD.
Pour les applications où la clé serait exposée côté client, préférez passer l’appel API via votre propre backend (BFF) qui détient la clé côté serveur.
Résultat final
Avec cette intégration, votre formulaire :
| Cas | Comportement |
|---|---|
Email valide (is_risky: false, score < 50) | Indicateur vert ✓, soumission autorisée |
| Email suspect (score 50–64) | Avertissement ⚠, soumission autorisée |
Typo détectée (did_you_mean) | Suggestion interactive, soumission autorisée |
Email risqué (is_risky: true) | Erreur ✗, soumission bloquée |
| API indisponible | Fail open, soumission autorisée |
| Email mal formaté | Erreur regex locale, pas d’appel API |
Pour aller plus loin
- Documentation API Syvel →
- Guide d’intégration JavaScript/TypeScript →
- Guide de démarrage complet →
- Guide Python FastAPI : voir l’article Valider des emails en Python →
- Comprendre les méthodes de validation : Regex, DNS, SMTP — quelle méthode choisir ?
- Pourquoi c’est important : Comment les emails jetables détruisent votre délivrabilité