jules@halfnil:~$ cat ~/blog/formulaire-de-contact-sans-service-tiers.md

Un formulaire de contact sans service tiers.

Base SQLite, honeypot silencieux, rate-limit et notification SMTP — tout ce qu'il faut pour un formulaire de contact fiable, sans dépendre de personne.

20 juillet 2026 · 2 min ·

Le contact de mon portfolio reposait sur un lien mailto:. Ça semble raisonnable jusqu'au jour où on réalise que la moitié des gens sont sur un webmail : le clic ouvre un client mail vide qu'ils n'ont jamais configuré, et le message n'arrive jamais.

Les solutions habituelles (Formspree, Getform…) marchent, mais je m'étais fixé une règle sur ce site : posséder toute la chaîne. Voici ce que ça donne.

## 1. Le message va en base, pas dans les limbes

Une table Prisma de quatre champs — nom, email, message, lu — et une route POST /api/contact validée par zod. Le message est d'abord persisté. Tout le reste (notification, email) est du bonus : si ça échoue, rien n'est perdu, le message m'attend dans l'onglet « messages » de mon panneau d'admin.

## 2. Le honeypot qui ment aux bots

Un champ website invisible pour les humains (hors écran, tabindex=-1), irrésistible pour les bots qui remplissent tout. Le détail qui compte : quand il est rempli, je ne renvoie pas d'erreur — je réponds {"ok": true} et je n'enregistre rien.

ts
// le bot croit avoir réussi ; il n'apprend rien
if (input.website) return ok({ ok: true }, { status: 201 });

Renvoyer une 400 explicite, c'est donner au bot le mode d'emploi pour passer la prochaine fois.

## 3. Rate-limit minimal, suffisant

Cinq messages par heure et par IP, dans une simple Map en mémoire. Ça se réinitialise au redémarrage du serveur et ça ne survivrait pas à plusieurs instances — mais pour un portfolio sur un VPS avec un seul process, c'est exactement le bon niveau de complexité.

## 4. La notification qui n'ajoute aucune latence

L'email de notification part après la réponse HTTP, grâce à after() de Next :

ts
after(() => sendContactNotification({ name, email, message }));

Le visiteur a sa confirmation immédiatement ; le SMTP prend le temps qu'il veut. Et le champ replyTo de l'email pointe sur l'adresse du visiteur : dans ma boîte, « répondre » répond à la bonne personne.

## Ce que je retiens

Un formulaire de contact, c'est quatre problèmes distincts : persister, filtrer, limiter, notifier. Les traiter séparément rend chaque morceau trivial — et aucun ne dépend d'un service que je ne contrôle pas.

## Pour aller plus loin