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 user0.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
- Go to the Google reCAPTCHA admin console.
- Create a new site.
- Choose reCAPTCHA v3.
- Add your app domains (for local development, add
localhost). - 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:
successshould betruescoreshould be greater than your threshold (example0.5)actionshould match expected action name (recommended)hostnameshould 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.
