React contact form secured with Cloudflare Turnstile

Building a Secure Contact Form with React, PHP & Cloudflare Turnstile

Master the process of building a secure and modern contact form using React, PHP, and Cloudflare Turnstile. Includes deep insights on frontend handling, server-side validation, and spam protection.

WebsitesSecurityTools
Intermediate | 9 min

2025-06-21

Building a Secure Contact Form with React, PHP & Cloudflare Turnstile

Creating a simple contact form is easy—but making it secure, user-friendly, and bot-resistant requires deeper thinking. This tutorial guides you through building a robust React-based contact form that communicates with a PHP backend and is protected from automated spam using Cloudflare Turnstile, an invisible CAPTCHA alternative.

🔐 What Makes a Contact Form 'Secure'?

  • Input sanitization and validation to avoid XSS and email injection
  • Spam and bot protection (via CAPTCHA or alternatives)
  • Rate limiting or throttling
  • Encrypted transmission over HTTPS
  • CSRF protection and backend authentication if needed

In this guide, we’ll focus primarily on frontend integration with Turnstile and a secure PHP backend that validates submissions and rejects bots.

🌐 Why Cloudflare Turnstile Instead of reCAPTCHA?

Cloudflare Turnstile is a modern alternative to Google reCAPTCHA. It's invisible, doesn’t track users, and doesn't require annoying image or puzzle challenges. It works by evaluating client-side browser interactions and signals to determine legitimacy, making it a frictionless experience for real users while blocking most bots.

🧱 React Form Setup with Turnstile Integration

The React component needs to collect user data and Turnstile's token, then send both to the backend. We'll dynamically render the Turnstile widget and capture its response token.

// Assumes Turnstile is loaded globally via a script tag
import { useRef, useEffect } from 'react';

export default function ContactForm() {
  const formRef = useRef();
  const turnstileRef = useRef();
  
  useEffect(() => {
    if (window.turnstile && !turnstileRef.current.hasChildNodes()) {
      window.turnstile.render(turnstileRef.current, {
        sitekey: 'your-site-key',
        callback: (token) => console.log('Turnstile token:', token)
      });
    }
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const token = document.querySelector('[name=cf-turnstile-response]')?.value;

    const payload = {
      name: formRef.current.name.value,
      email: formRef.current.email.value,
      message: formRef.current.message.value,
      'cf-turnstile-response': token
    };

    const res = await fetch('/backend/contact.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });

    const result = await res.json();
    alert(result.message);
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Your Message" required></textarea>
      <div ref={turnstileRef} className="cf-turnstile" data-sitekey="your-site-key"></div>
      <button type="submit">Send</button>
    </form>
  );
}

🧪 Backend Validation in PHP with Turnstile API

Your backend must verify the Turnstile token using Cloudflare’s API to confirm the request wasn't made by a bot. After validation, sanitize the data before processing or storing.

<?php
$data = json_decode(file_get_contents("php://input"), true);

$responseToken = $data['cf-turnstile-response'] ?? '';

$verifyResponse = file_get_contents('https://challenges.cloudflare.com/turnstile/v0/siteverify', false, stream_context_create([
  'http' => [
    'method' => 'POST',
    'header' => 'Content-type: application/x-www-form-urlencoded',
    'content' => http_build_query([
      'secret' => 'your-secret-key',
      'response' => $responseToken
    ])
  ]
]));

$result = json_decode($verifyResponse, true);

if (!$result['success']) {
  echo json_encode(['message' => 'Turnstile verification failed.']);
  exit;
}

$name = htmlspecialchars($data['name']);
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
$message = htmlspecialchars($data['message']);

if (!$email) {
  echo json_encode(['message' => 'Invalid email address.']);
  exit;
}

// Optionally log to DB, send email or push to CRM
// Example:
// mail("admin@example.com", "New Contact", $message);

echo json_encode(['message' => 'Form submitted successfully.']);
?>

📈 Best Practices for Production

  • Use environment variables to store your secret keys securely.
  • Implement logging and alerting for suspicious or high-frequency requests.
  • Ensure HTTPS is enforced and all form data is encrypted in transit.
  • Add additional server-side checks like length validation and content filtering.
  • Rate-limit submissions per IP to prevent abuse.

While Turnstile filters out most bots, combining it with server-side logic greatly improves security and gives you more control.

Download the full source code

Back to blogs