How to Use Googleâs ReCaptcha V3 with NextJS 13 and the New App Router
Introduction
CAPTCHA, which stands for âCompletely Automated Public Turing test to tell Computers and Humans Apart,â is a way to differentiate between human users and bots. This functionality is especially crucial for form submissions to prevent spam and abuse. Invisible CAPTCHA doesnât interrupt the user flow and performs these checks seamlessly in the background.
Google ReCaptcha V3
Google ReCaptcha V3 is an excellent example of an invisible CAPTCHA. After you include some JavaScript from Google on your page, that script will add a token to your form submission. Then, on the server-side, youâll verify this token using a secret key that only the server knows. If the CAPTCHA verification passes, youâre clear to proceed with operations like registering a user.
How It Works
- Include Googleâs ReCaptcha JavaScript on your webpage.
- On form submit, a token is added automatically by the ReCaptcha script.
- Send this token along with your form data to the server.
- Server-side: verify this token using Googleâs secret key.
- If it passes, proceed with further processing.
NextJS 13âs App Router
NextJS 13 introduces a new folder structure, notably different from the older pages
directory approach. The new structure looks something like this:
- app/
- api/
- contactFormSubmit/
- route.ts
- public/
- page.tsx
- google-captcha-wrapper.tsx
Here, /app/api/contactFormSubmit/route.ts
is where your server-side logic resides. Specifically, this is the server-side handler that listens to HTTP POST requests from the form on your website. This form submission will include a token generated by Google ReCaptcha V3.
This co-location feature allows your server-side logic to reside closer to your client-side logic. Essentially, you can have server-side and client-side logic co-located, making your project easier to navigate and manage.
Setting Up Google ReCaptcha V3 and Environment Variables
Before we deep-dive into the code, itâs crucial to register your app on Googleâs Developer Portal and get the keys for ReCaptcha V3.
-
Google Developer Portal: Head over to the Google ReCaptcha Website and click on the âAdmin Consoleâ button. Sign in with your Google account if you havenât already.
-
Create a New Site: Once in the console, hit the â+â button to create a new site. Choose âReCaptcha V3â, give your domain, and note down the keys youâll receive. There are two keys: one is the site key and the other is the secret key.
Now, letâs bring those keys into our Next.js application environment.
Creating .env file
In the root directory of your NextJS project, create a new file named .env
. This is where weâll place our keys securely. Your .env
file will look something like this:
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LeFZTIoAAAAA...
RECAPTCHA_SECRET_KEY=6LeFZTIoAAAA...
In our codebase, youâll notice that these keys are read through process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY
and process.env.RECAPTCHA_SECRET_KEY
.
Note: Prefixing an environment variable with NEXT_PUBLIC_
exposes it to the client-side code. So, NEXT_PUBLIC_RECAPTCHA_SITE_KEY
is accessible in your client-side components. Itâs a naming convention Next.js uses to inject runtime environment variables.
Thatâs it! Youâve set up the Google Developer Portal and safely stored the environment variables for use in your Next.js application. Now, letâs plug these keys into our code.
Implementation Example
To use the code snippets below, youâll need to install the npm package react-google-recaptcha-v3
.
Also, if you want to try this example yourself, all the code can be found at this URL: https://github.com/pkellner/nextjs-google-recaptcha-v3-app-router-demo
Client-side Code
In /app/page.tsx
, use the use client
directive to run this file in the browser. Omitting this directive would treat the component as a server component, which is beyond the scope of this article.
Hereâs the code that runs in your browser:
// Code for /app/page.tsx (see below for full code listing)
And itâs wrapped with this provider:
// Code for GoogleCaptchaWrapper (see below for full code listing)
Note that GoogleReCaptchaProvider
must wrap any component that uses the hook useGoogleReCaptcha
. The HomeInside
component is required for better code organization.
Server-side Code
Here is the handler code that runs when the user submits the form:
// Code for /app/api/contactFormSubmit/route.ts (see below for full code listing)
The server-side handler listens for a POST
request at http://localhost:3000/api/contactFormSubmit
when youâre running the app locally.
Wrapping Up
Youâve made it to the end, and what a journey itâs been! Weâve delved into the intricacies of Google ReCaptcha V3, explored Invisible CAPTCHA, and even got our hands dirty with NextJS 13âs new App Router. The code snippets should guide you in crafting a smoother and more secure user experience, which is always a win-win.
If youâve enjoyed this read as much as Iâve enjoyed writing it, thatâs a success in my book. Until next time, code on! đ
Full Code Listings
/app/page.tsx
"use client";
import React, { useState } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import axios from "axios";
import GoogleCaptchaWrapper from "@/app/google-captcha-wrapper";
interface PostData {
gRecaptchaToken: string;
firstName: string;
lastName: string;
email: string;
hearFromSponsors: boolean;
}
export default function Home() {
return (
<GoogleCaptchaWrapper>
<HomeInside />
</GoogleCaptchaWrapper>
);
}
function HomeInside() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [hearFromSponsors, setHearFromSponsors] = useState(false);
const [notification, setNotification] = useState('');
const { executeRecaptcha } = useGoogleReCaptcha();
const handleSubmitForm = function (e: any) {
e.preventDefault();
if (!executeRecaptcha) {
console.log("Execute recaptcha not available yet");
setNotification(
"Execute recaptcha not available yet likely meaning key not recaptcha key not set"
);
return;
}
executeRecaptcha("enquiryFormSubmit").then((gReCaptchaToken) => {
submitEnquiryForm(gReCaptchaToken);
});
};
const submitEnquiryForm = (gReCaptchaToken : string) => {
async function goAsync() {
const response = await axios({
method: "post",
url: "/api/contactFormSubmit",
data: {
firstName: firstName,
lastName: lastName,
email: email,
hearFromSponsors: hearFromSponsors,
gRecaptchaToken: gReCaptchaToken,
},
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
});
if (response?.data?.success === true) {
setNotification(`Success with score: ${response?.data?.score}`);
} else {
setNotification(`Failure with score: ${response?.data?.score}`);
}
}
goAsync().then(() => {}); // suppress typescript error
};
return (
<div className="container">
<main className="mt-5"> {/* Add a top margin for better spacing */}
<h2>Interested in Silicon Valley Code Camp</h2>
<form onSubmit={handleSubmitForm}>
<div className="mb-3">
<input
type="text"
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e?.target?.value)}
className="form-control"
placeholder="First Name"
/>
</div>
<div className="mb-3">
<input
type="text"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e?.target?.value)}
className="form-control"
placeholder="Last Name"
/>
</div>
<div className="mb-3">
<input
type="text"
name="email"
value={email}
onChange={(e) => setEmail(e?.target?.value)}
className="form-control"
placeholder="Email Address"
/>
</div>
<div className="mb-3 form-check">
<input
type="checkbox"
name="hearFromSponsors"
checked={hearFromSponsors}
onChange={(e) => setHearFromSponsors(e?.target?.checked)}
className="form-check-input"
/>
<label className="form-check-label">Hear from our sponsors</label>
</div>
<button type="submit" className="btn btn-light">Submit</button>
{notification && <p className="mt-3 text-info">{notification}</p>}
</form>
</main>
</div>
);
}
/app/google-captcha-wrapper.tsx
"use client";
import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import React from "react";
export default function GoogleCaptchaWrapper({
children,
}: {
children: React.ReactNode;
}) {
const recaptchaKey: string | undefined =
process?.env?.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
return (
<GoogleReCaptchaProvider
reCaptchaKey={recaptchaKey ?? "NOT DEFINED"}
scriptProps={{
async: false,
defer: false,
appendTo: "head",
nonce: undefined,
}}
>
{children}
</GoogleReCaptchaProvider>
);
}
/app/api/contactFormSubmit/route.ts
import { NextResponse } from "next/server";
import axios from "axios";
export async function POST(request: Request, response: Response) {
const secretKey = process?.env?.RECAPTCHA_SECRET_KEY;
const postData = await request.json();
const { gRecaptchaToken, firstName, lastName, email, hearFromSponsors } =
postData;
console.log(
"gRecaptchaToken,firstName,lastName,email,hearFromSponsors:",
gRecaptchaToken?.slice(0, 10) + "...",
firstName,
lastName,
email,
hearFromSponsors
);
let res: any;
const formData = `secret=${secretKey}&response=${gRecaptchaToken}`;
try {
res = await axios.post(
"https://www.google.com/recaptcha/api/siteverify",
formData,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
} catch (e) {
console.log("recaptcha error:", e);
}
if (res && res.data?.success && res.data?.score > 0.5) {
// Save data to the database from here
console.log("Saving data to the database:", firstName, lastName, email, hearFromSponsors);
console.log("res.data?.score:", res.data?.score);
return NextResponse.json({
success: true,
firstName, lastName,
score: res.data?.score,
});
} else {
console.log("fail: res.data?.score:", res.data?.score);
return NextResponse.json({ success: false, name, score: res.data?.score });
}
}