Back to Blog
Tutorials

How to Integrate Google reCAPTCHA v3 in a React App (Frontend + Backend)

A simple step-by-step guide to add Google reCAPTCHA v3 in a React app, send the token to your backend, and validate the score securely before processing a form submission.

Feb 2, 2026
How to Integrate Google reCAPTCHA v3 in a React App (Frontend + Backend)

How to Integrate Google reCAPTCHA v3 in a React App (Frontend + Backend)

If you have a contact form, signup form, or any public form, spam bots can abuse it.

Google reCAPTCHA v3 helps reduce this by giving each request a score between 0.0 and 1.0:

  • 1.0 = likely a real user
  • 0.0 = likely a bot

In this guide, you will learn how to:

  • Set up reCAPTCHA v3 keys
  • Generate token on the React frontend
  • Send token to backend
  • Verify token and score on backend

Prerequisites

Before coding, keep these ready:

  • A Google account
  • A React app (Vite, CRA, Next.js client component, anything React)
  • A backend API (Node/Express, Next.js API route, etc.)
  • A form to protect (for example: contact form)

1) Create reCAPTCHA v3 keys

  1. Go to the Google reCAPTCHA admin console.
  2. Create a new site.
  3. Choose reCAPTCHA v3.
  4. Add your app domains (for local development, add localhost).
  5. Copy:
    • Site Key (public, used on frontend)
    • Secret Key (private, only on backend)

Important Concept (Very Short)

You should never trust frontend validation alone.

Frontend only creates a token. The real decision happens on backend by calling Google verify API with your secret key.


Frontend Integration (React)

We will use react-google-recaptcha-v3.

Install package

npm install react-google-recaptcha-v3

Add environment variable

Create .env (or .env.local) in your React app:

VITE_RECAPTCHA_SITE_KEY=your_site_key_here

If you are using CRA, use REACT_APP_RECAPTCHA_SITE_KEY. If you are using Next.js client code, use NEXT_PUBLIC_RECAPTCHA_SITE_KEY.

Wrap app with provider

import React from "react";
import ReactDOM from "react-dom/client";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")!).render(
	<React.StrictMode>
		<GoogleReCaptchaProvider reCaptchaKey={import.meta.env.VITE_RECAPTCHA_SITE_KEY}>
			<App />
		</GoogleReCaptchaProvider>
	</React.StrictMode>
);

Use token in form submit

import { FormEvent, useState } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";

export default function ContactForm() {
	const { executeRecaptcha } = useGoogleReCaptcha();
	const [loading, setLoading] = useState(false);

	async function onSubmit(event: FormEvent<HTMLFormElement>) {
		event.preventDefault();

		if (!executeRecaptcha) {
			alert("reCAPTCHA is still loading. Please try again.");
			return;
		}

		setLoading(true);

		try {
			const formData = new FormData(event.currentTarget);
			const name = String(formData.get("name") || "");
			const message = String(formData.get("message") || "");

			// Action name helps you track intent on Google dashboard
			const captchaToken = await executeRecaptcha("contact_form_submit");

			const response = await fetch("/api/contact", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					name,
					message,
					captchaToken,
				}),
			});

			const result = await response.json();

			if (!response.ok) {
				throw new Error(result.message || "Submission failed");
			}

			alert("Form submitted successfully");
			event.currentTarget.reset();
		} catch (error) {
			console.error(error);
			alert("Could not submit form");
		} finally {
			setLoading(false);
		}
	}

	return (
		<form onSubmit={onSubmit}>
			<input name="name" placeholder="Your name" required />
			<textarea name="message" placeholder="Your message" required />
			<button type="submit" disabled={loading}>
				{loading ? "Sending..." : "Send"}
			</button>
		</form>
	);
}

Backend Validation (Node/Express Example)

Now validate token on server using your Secret Key.

Backend env variable

RECAPTCHA_SECRET_KEY=your_secret_key_here

Express route example

import express from "express";

const app = express();
app.use(express.json());

app.post("/api/contact", async (req, res) => {
	try {
		const { name, message, captchaToken } = req.body as {
			name: string;
			message: string;
			captchaToken: string;
		};

		if (!name || !message || !captchaToken) {
			return res.status(400).json({ message: "Missing required fields" });
		}

		const verifyResponse = await fetch("https://www.google.com/recaptcha/api/siteverify", {
			method: "POST",
			headers: { "Content-Type": "application/x-www-form-urlencoded" },
			body: new URLSearchParams({
				secret: process.env.RECAPTCHA_SECRET_KEY || "",
				response: captchaToken,
			}),
		});

		const verifyData = (await verifyResponse.json()) as {
			success: boolean;
			score: number;
			action?: string;
			hostname?: string;
			challenge_ts?: string;
			"error-codes"?: string[];
		};

		// Choose threshold based on your app risk level
		const SCORE_THRESHOLD = 0.5;

		const isValid =
			verifyData.success &&
			typeof verifyData.score === "number" &&
			verifyData.score >= SCORE_THRESHOLD;

		if (!isValid) {
			return res.status(403).json({
				message: "Captcha validation failed",
				score: verifyData.score,
			});
		}

		// Captcha is valid -> continue real business logic
		// e.g. save message, send email, etc.
		return res.status(200).json({
			message: "Message accepted",
			score: verifyData.score,
		});
	} catch (error) {
		console.error("Contact API error:", error);
		return res.status(500).json({ message: "Internal server error" });
	}
});

How Score Validation Works

On backend, check at least these fields:

  • success should be true
  • score should be greater than your threshold (example 0.5)
  • action should match expected action name (recommended)
  • hostname should match your domain in production (recommended)

If score is low, you can:

  • Reject request directly
  • Ask for extra verification
  • Rate-limit user/IP

Next.js Backend Version (Optional)

If your React app is in Next.js App Router, backend route usually looks like this:

// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
	const { name, message, captchaToken } = await req.json();

	if (!name || !message || !captchaToken) {
		return NextResponse.json({ message: "Missing required fields" }, { status: 400 });
	}

	const verifyResponse = await fetch("https://www.google.com/recaptcha/api/siteverify", {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			secret: process.env.RECAPTCHA_SECRET_KEY || "",
			response: captchaToken,
		}),
	});

	const verifyData = await verifyResponse.json();
	const SCORE_THRESHOLD = 0.5;

	if (!verifyData.success || verifyData.score < SCORE_THRESHOLD) {
		return NextResponse.json(
			{ message: "Captcha validation failed", score: verifyData.score },
			{ status: 403 }
		);
	}

	return NextResponse.json({ message: "Message accepted", score: verifyData.score });
}

Common Mistakes to Avoid

  • Using secret key on frontend (never do this)
  • Accepting form without backend verification
  • Not checking score threshold
  • Forgetting to add production domain in Google console
  • Running ad blockers/privacy extensions that block captcha script in development

Final Checklist

  • Site key in frontend env
  • Secret key in backend env
  • Token generated with executeRecaptcha
  • Token sent to backend with form payload
  • Backend verifies token via Google API
  • Backend checks score and accepts/rejects request

Once this is done, your form is much better protected against bots while keeping UX smooth for real users.