Gloweet
    • Projects
    • Contact me
    • About me
    • Bio
    • Expertises
    • Stay updated
    • Blogs
    • Newsletter
Language
Language
New project
Antonin Marxer's Blog
7 Ways to Stop Form Spam in Remix / Node.js
7 Ways to Stop Form Spam in Remix / Node.js

7 Ways to Stop Form Spam in Remix / Node.js

Flag bot activity, use built-in rate limit APIs, prevent bounced emails

Antonin Marxer

·

Last updated on August 12, 2025

·

5min read

Table of contents

  • Quick summary

Your contact page or your newsletter form is all set, ready for new visitors.
You launch your website, and after a few days here comes the drama: 1000 form submissions, all by the same bot activity.

I know I’m not that famous to reach those numbers 🫠 Let’s check the my dashboard one second…

Resend email dashboard showing bot activity

What a funny guy that RobertMow and his 990 SUBMISSIONS
You know where this is going: let’s secure our endpoint!

In this tutorial we’ll go over 7 tips and their implementation to prevent bot form submissions.

  1. Flag bots IP addresses

When an activity can only be a bot, we can flag IP addresses by storing them as malicious. The storage mechanism depends on your stack:

  • On Cloudflare Workers, use KV (key-value datastore)

  • On any other backend, use server-side session

We’ll see how we can notice a bot activity in the next steps.
Once the ip address if flagged, ignore any form sumbit:

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// prevent bots from re-submitting a form&lt;/span&gt;</span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable constant_&quot;</span>&gt;</span>KV<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> = context.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;cloudflare&lt;<span class="hljs-regexp">/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/</span>span&gt;.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;<span class="hljs-variable constant_">KV</span>&lt;/span&gt;;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> ipKey = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span>`contact_ip_<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-subst&quot;</span>&gt;</span>${clientIP}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>`<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable constant_&quot;</span>&gt;</span>KV<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;get&lt;/span&gt;(ipKey)) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable language_&quot;</span>&gt;</span>console<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;warn&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;`IP &lt;span class=&quot;hljs-subst&quot;&gt;${clientIP}&lt;/</span>span&gt; has been flagged <span class="hljs-keyword">as</span> a bot<span class="hljs-string">`&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Response&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;json&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;success&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; });
}</span>
  1. Prevent bypassing client-side validation

    If not alrejady done, please ensure your form is validated on client-side.
    In addition, if the value sent to the server isn’t validated, that means client-side validation has been bypassed and should be flagged as malicious.
&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;<span class="hljs-keyword">if</span>&lt;<span class="hljs-regexp">/span&gt; (!(&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/</span>span&gt; schema.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;validate&lt;/span&gt;(data))) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable language_&quot;</span>&gt;</span>console<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;warn&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Client-side form validation bypassed&amp;quot;&lt;/</span>span&gt;);
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable constant_&quot;</span>&gt;</span>KV<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;put&lt;<span class="hljs-regexp">/span&gt;(ipKey, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;bot&amp;quot;&lt;/</span>span&gt;, { <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>expirationTtl<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>86400<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> }); <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// 24 hours cooldown<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;json&lt;<span class="hljs-regexp">/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;success&lt;/</span>span&gt;: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> });
}
  1. Use a client nonce

    To prevent replay attacks, we generate a unique nonce for each form submission. This nonce is stored on backend's side and compared against the nonce provided by the client.
&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// generate nonce on contact form page render&lt;/span&gt;</span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> timestamp = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Date<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;now&lt;/span&gt;();
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> randomPart = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Math<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;random&lt;<span class="hljs-regexp">/span&gt;().&lt;span class=&quot;hljs-title function_&quot;&gt;toString&lt;/</span>span&gt;(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>36<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>).&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;substring&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/</span>span&gt;, <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>15<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>);
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> nonce = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span>`<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-subst&quot;</span>&gt;</span>${timestamp}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>.<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-subst&quot;</span>&gt;</span>${randomPart}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>`<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable constant_&quot;</span>&gt;</span>KV<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;put&lt;<span class="hljs-regexp">/span&gt;(nonceKey, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;unused&amp;quot;&lt;/</span>span&gt;)
&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">/**
 * Validates if a nonce is valid and not expired
 * &lt;span class=&quot;hljs-doctag&quot;&gt;<span class="hljs-doctag">@param</span>&lt;/span&gt; nonce The nonce to validate
 * &lt;span class=&quot;hljs-doctag&quot;&gt;<span class="hljs-doctag">@param</span>&lt;/span&gt; maxAge Maximum age of the nonce in milliseconds (default: 1 hour)
 * &lt;span class=&quot;hljs-doctag&quot;&gt;<span class="hljs-doctag">@returns</span>&lt;/span&gt; Boolean indicating if the nonce is still valid
 */</span>&lt;/span&gt;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>export<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>function<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;</span>isNonceValid<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-params&quot;</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>nonce<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>: <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-built_in&quot;</span>&gt;</span>string<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>, <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>maxAge<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>: <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-built_in&quot;</span>&gt;</span>number<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span> = <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>3600000<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>): <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-built_in&quot;</span>&gt;</span>boolean<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>try<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> {
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> parts = nonce.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;split&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;.&amp;quot;&lt;/</span>span&gt;);
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (parts.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;length&lt;<span class="hljs-regexp">/span&gt; !== &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/</span>span&gt;) &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;<span class="hljs-keyword">return</span>&lt;<span class="hljs-regexp">/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/</span>span&gt;;

    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> timestamp = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-built_in&quot;</span>&gt;</span>parseInt<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>(parts[<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>], <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>10<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>);
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> now = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Date<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;now&lt;/span&gt;();

    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// Check if nonce has expired<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> now - timestamp &amp;lt;= maxAge;
  } &lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;<span class="hljs-keyword">catch</span>&lt;/span&gt; (error) {
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
  }
}

...

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;<span class="hljs-keyword">if</span>&lt;<span class="hljs-regexp">/span&gt; (!&lt;span class=&quot;hljs-title function_&quot;&gt;isNonceValid&lt;/</span>span&gt;(nonce)) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable language_&quot;</span>&gt;</span>console<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;warn&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Expired nonce detected&amp;quot;&lt;/</span>span&gt;);
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;json&lt;/span&gt;({
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>success<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>error<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;</span>t<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>contact.response.error.sessionExpired<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>),
  });
}
&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;<span class="hljs-keyword">const</span>&lt;<span class="hljs-regexp">/span&gt; nonceKey = &lt;span class=&quot;hljs-string&quot;&gt;`contact_nonce_&lt;span class=&quot;hljs-subst&quot;&gt;${nonce}&lt;/</span>span&gt;<span class="hljs-string">`&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; nonceState = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;KV&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(nonceKey);
&lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (nonceState === &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;unused&amp;quot;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;KV&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;put&lt;/span&gt;(nonceKey, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;used&amp;quot;&lt;/span&gt;);
} &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (nonceState === &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;used&amp;quot;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;warn&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Duplicate form submission&amp;quot;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Response&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;json&lt;/span&gt;(
    {
      &lt;span class=&quot;hljs-attr&quot;&gt;success&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;,
      &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;The form has already been submitted&amp;quot;&lt;/span&gt;
    },
    { &lt;span class=&quot;hljs-attr&quot;&gt;status&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;409&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;headers&lt;/span&gt;: { &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt; } }
  );
} &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;warn&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Invalid nonce&amp;quot;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Response&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;json&lt;/span&gt;(
    {
      &lt;span class=&quot;hljs-attr&quot;&gt;success&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;,
      &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Invalid nonce submitted&amp;quot;&lt;/span&gt;
    },
    { &lt;span class=&quot;hljs-attr&quot;&gt;status&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;409&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;headers&lt;/span&gt;: { &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt; } }
  );
}</span>
  1. Cloudflare Rate Limiting

We use Cloudflare's Rate Limiting API to restrict the number of form submissions from a single IP address. Note 1: This is not perfect as users on mobile networks often have the same IP address. Note 2: The rate limiting API is still in open beta (August 2025).

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// Implementation in index.tsx&lt;/span&gt;</span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> rateLimiter = context.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;cloudflare&lt;<span class="hljs-regexp">/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/</span>span&gt;.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;<span class="hljs-variable constant_">RATE_LIMITER_CONTACT</span>&lt;/span&gt;;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> clientIP = request.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;headers&lt;<span class="hljs-regexp">/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/</span>span&gt;(<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>CF-Connecting-IP<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>) || <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span><span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> rateLimitResult = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> rateLimiter.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;limit&lt;<span class="hljs-regexp">/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;key&lt;/</span>span&gt;: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span>`contact_form_<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-subst&quot;</span>&gt;</span>${clientIP}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>`<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>});
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (!rateLimitResult.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;success&lt;/span&gt;) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// Return 429 Too Many Requests<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
}

The Rate Limiter needs to be bound to your Cloudflare Worker environment. This is typically done in your wrangler.toml file:

&lt;span <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-section&quot;</span>&gt;[env.production]&lt;/span&gt;
&lt;span <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;kv_namespaces&lt;/span&gt; = [
  { binding = &lt;span class=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;&amp;quot<span class="hljs-comment">;RATE_LIMITER_CONTACT&amp;quot;&lt;/span&gt;, id = &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;unique-namespace-id&amp;quot;&lt;/span&gt; }</span>
]
  1. Honeypot Field

A hidden field called "website" is included in the form. This field is invisible to human users but will likely be filled out by bots. If the field contains any data, the submission is silently rejected.

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// Check honeypot field - if it contains data, it&amp;#x27;s likely a bot&lt;/span&gt;</span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> honeypotField = data.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;website&lt;<span class="hljs-regexp">/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;toString&lt;/</span>span&gt;();
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (honeypotField) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable language_&quot;</span>&gt;</span>console<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;warn&lt;<span class="hljs-regexp">/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Honeypot field triggered&amp;quot;&lt;/</span>span&gt;);
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-variable constant_&quot;</span>&gt;</span>KV<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;put&lt;<span class="hljs-regexp">/span&gt;(ipKey, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;bot&amp;quot;&lt;/</span>span&gt;, { <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>expirationTtl<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-number&quot;</span>&gt;</span>86400<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> }); <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// 24 hours cooldown      <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// Return success to avoid giving bots feedback, but don<span class="hljs-symbol">&amp;#x27;</span>t process the form<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;json&lt;<span class="hljs-regexp">/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;success&lt;/</span>span&gt;: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> });
}
  1. CAPTCHA Integration (hCaptcha)

The form includes an hCaptcha challenge to verify that the user is human. We use the official React component from @hcaptcha/react-hcaptcha.

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// In the form component&lt;/span&gt;</span>
&amp;lt;<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>HCaptcha<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  ref={captchaRef}
  sitekey={process.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;env&lt;<span class="hljs-regexp">/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;HCAPTCHA_SITE_KEY&lt;/</span>span&gt; || <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>10000000-ffff-ffff-ffff-000000000001<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>}
  onVerify={<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-function&quot;</span>&gt;</span>(<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-params&quot;</span>&gt;</span>token<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>) =<span class="hljs-symbol">&amp;gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;</span>setCaptchaToken<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>(token)}
/&amp;gt;

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;</span>// In the submission handling<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> captchaResult = <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>await<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;</span>validateCaptcha<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>(data.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;hCaptchaToken&lt;<span class="hljs-regexp">/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;toString&lt;/</span>span&gt;());
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (!captchaResult.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;success&lt;/span&gt;) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;json&lt;/span&gt;({
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>success<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>error<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>CAPTCHA verification failed<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
  });
}

hCaptcha requires a site key and secret key to be set as environment variables in the .env file (locally) and in your wrangler.jsonc file (on Cloudflare Workers).

  • HCAPTCHA_SITE_KEY - Your hCaptcha site key for frontend integration

  • HCAPTCHA_SECRET_KEY - Your hCaptcha secret key for verification

  1. Email Validation with Mailchecker

We use the mailchecker library to validate email addresses and detect disposable email services. It is backed by a database of over 55,000 throwable email domains.

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-comment&quot;</span>&gt;<span class="hljs-comment">// Validate email with mailchecker&lt;/span&gt;</span>
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>const<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> email = data.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;email&lt;<span class="hljs-regexp">/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;toString&lt;/</span>span&gt;() || <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span><span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>if<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (email &amp;amp;&amp;amp; !mailchecker.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;isValid&lt;/span&gt;(email)) {
  <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-keyword&quot;</span>&gt;</span>return<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-title class_&quot;</span>&gt;</span>Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;json&lt;/span&gt;({
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>success<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-literal&quot;</span>&gt;</span>false<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;</span>error<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>: <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>Please use a valid email address<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
  });
}

Additionally, email validation is also included in the Yup schema:

&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-attr&quot;</span>&gt;email&lt;/span&gt;: yup
  .&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;<span class="hljs-built_in">string</span>&lt;/span&gt;()
  .&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;email&lt;<span class="hljs-regexp">/span&gt;(messages.&lt;span class=&quot;hljs-property&quot;&gt;email&lt;/</span>span&gt;.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;invalid&lt;/span&gt;)
  .&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;required&lt;<span class="hljs-regexp">/span&gt;(messages.&lt;span class=&quot;hljs-property&quot;&gt;email&lt;/</span>span&gt;.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-property&quot;</span>&gt;required&lt;/span&gt;)
  .&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;test&lt;/span&gt;(
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>is-valid-email<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-string&quot;</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>Email appears to be invalid or disposable<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>,
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-function&quot;</span>&gt;</span>(<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">&quot;hljs-params&quot;</span>&gt;</span>value<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>) =<span class="hljs-symbol">&amp;gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span> (value ? mailchecker.&lt;span <span class="hljs-keyword">class</span>=<span class="hljs-string">&quot;hljs-title function_&quot;</span>&gt;isValid&lt;<span class="hljs-regexp">/span&gt;(value) : &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/</span>span&gt;)
  ),
  1. Avoid false positive

    I do not suggest using content filtering, for example on keywords (“nigerian”, “prince”) as this may result in false positives.

Quick summary

If you’re in a hurry, captchas can still be a decent option — most bots don’t handle them well.
Just make sure you check the captcha value on the server so it can’t be skipped.

That’s what’s worked for me against spam, hopefully it’s useful for you too.

Have you run into malicious activity, DDoS attacks, or spammed forms?
I’m curious how you solved it — and I wouldn’t mind hearing the weird or funny stories that came with it.

Table of contents

  • Quick summary
Remixcloudflare-workerNode.jsspamanti-spamformsValidationrate-limitcaptcha

Comments

Be the first to comment!

More Articles

Create your own MCP servers

Create your own MCP servers

Discover how to design and build secure MCP servers using Typescript or Python.

fastmcp
cloudflare workers
typescripttypescript
Python
Streamable HTTP
17min read
Last updated 9 months ago
MCP servers - Introduction & Guide

MCP servers - Introduction & Guide

In this article, I explain how MCPs (Model Context Protocols) work and how to integrate them into your IDEs to connect external tools to your agent, providing it with context and levers for action to reduce redundant tasks -> Your IDE becomes a true all-in-one tool.

MCP
LLM
JSON
Stdio
SSE
Streamable HTTP
+18 more
6min read
Last updated 9 months ago
Cloud Email Microservices: A Guide to Using AWS Lambda and Cloudflare Workers

Cloud Email Microservices: A Guide to Using AWS Lambda and Cloudflare Workers

Deploy an email microservice on Lambda and handle queues — invoke from Cloudflare Workers or any Node.js backend

AWS Lambda
AWS SQS
remix.runremix.run
Cloudflare Workers
CloudFormation
SAM
15min read
Last updated 10 months ago
Best Practices for an Optimized Contact Page Design

Best Practices for an Optimized Contact Page Design

Build a Contact Page That Connects — and Blocks Spam

4min read
Last updated 10 months ago
Send and Receive Custom Domain Emails for Free

Send and Receive Custom Domain Emails for Free

Set up free professional email addresses like you@yourdomain.com without hosting a mail server or paying for Google Workspace. This guide shows how to receive emails using Forward Email and send as your custom domain via Gmail — fast, reliable, and 100% free.

Forward Email
Gmail
Google App Passwords
DNS
SPF
TLS
+1 more
Last updated 10 months ago
CI/CD deep-dive: Deploy a scalable Multi-Environment React App to AWS S3 + CloudFront with GitHub Actions

CI/CD deep-dive: Deploy a scalable Multi-Environment React App to AWS S3 + CloudFront with GitHub Actions

Learn how to build and deploy a scalable multi-environment React + Vite app using Domain-Driven Design, GitHub Actions for CI/CD, and AWS S3 + CloudFront for fast, cost-effective static hosting. Includes environment-specific configs, branch-based workflows, and secure AWS deployment setup.

Vite
ReactReact
S3
CloudFront
TailwindTailwind
Shadcn
+1 more
10min read
Last updated 9 months ago
0

Offer

  • Contact
  • Skills

Resources

  • Blog
  • Newsletter
  • About

Legal

  • Privacy
  • Terms of service
  • Cookies policy
  • Update cookie consent
  • LinkedIn
  • GitHub

Gloweet © 2025. All rights reserved.

Smile, you're alive :)

2.0.0