How to protect your Next.js Routes with reCAPTCHA

The easiest way to protect your website.

 · 

5 min read

notion-image
Protecting the public endpoints of your web app, is one of the most important tasks you could do.
Even if you don’t expect much traffic on your websites, malicious attempts can always happen.
 
It happened to me when I launched a waitlist website, I didn’t expect many eyes to visit the page, but someone noticed the `/api/waitlist` endpoint, I used to collect the email of the interested users, and they started to call it repeatedly.
 
One of the easiest mitigations is to add a captcha challenge to the web user interface.
 
There are different types of captchas, and they have evolved quite a lot over the last few years.
Usually, they are visual quizzes or simple puzzles (called challenges) to solve to unlock a feature on a website.
 
This is an example, select all square images with traffic lights.
notion image
 
Robots can’t solve these puzzles, therefore the backend request doesn’t start.
But how can you protect your backend with a puzzle solved on the frontend?
 

How Captcha Protection Works

 
This is how it works.
When the puzzle is successfully solved, the captcha service delivers a token (a string).
This token is unique for the puzzle resolution of a user, and you need to send it to your backend.
In your backend, you need to validate the token, by calling the captcha service backend.
In this article, I show you how you can implement a captcha protection using on the most famous service reCAPTCHA by Google and your Next.js website.
reCAPTCHA by Google has been improved over time, and the latest version of it, version 3, doesn’t require every user to solve the challenge is the proprietary algorithm of Google doesn’t recognize a suspect client.
Which is a great news for our real users!
 

Create a reCAPTCHA

First of all, create a reCAPTCHA. Visit the website https://www.google.com/recaptcha/about/ and access the v3 Admin Console.
Once in, click on the “+” plus icon to create a new reCAPTCHA.
Add the label and the domain of your website.
notion image
 
You can select v3 (score based) or v2.
v2 always asks for a challenge, while v3 asks for a challenge only if the user score, automatically calculated, is not high. I select v3.
Click on Submit.
 
Now Google gives you the Site Key and the Secret Key. Copy them in a secure place.
notion image
 
Now let’s create two environment variables.
Usually you have a .env file for your local development and you need to set them on your hosting solution, like Vercel.
 
YAML
NEXT_PUBLIC_RECAPTCHA_SITE_KEY="you_site_key"
RECAPTCHA_SECRET_KEY="your_secret_key"
 
Notice that one environment variables is prefixed with NEXT_PUBLIC_ while the other not.
NEXT_PUBLIC_RECAPTCHA_SITE_KEY is accessible from the frontend, and therefore it’s publicly visible, while RECAPTCHA_SECRET_KEY will be accessible only by the backend code, and therefore no one can read the value.
 

Create your client component

Now, let’s create a Next.js client component with an input field and a button.
For a waitlist it would look like this one:
TypeScript
<input 
    placeholder="[email protected]"
    onChange={e => setEmail(e.target.value)}
/>
<button
		onClick={onAddToWaitlist}
>
    Join the waitlist
</button>
 
Now, we need to implement onAddToWaitlist so that it calls the reCAPTCHA service.
 
To integrate reCAPTCHA you need to integrate the Google JavaScript script.
Open your layout.tsx file (if you are using the App Router) and add this
TypeScript
<script
	defer
  type="text/javascript"
  src={`https://www.google.com/recaptcha/api.js?render=${process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}`}
/>
At this point, the JavaScript object grecaptcha will be globally available in our web app.
 
Let’s implement onAddToWaitlist
TypeScript
const onAddToWaitlist = () => {
    // @ts-ignore
    grecaptcha.ready(function () {
      // @ts-ignore
      grecaptcha
        .execute(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, {
          action: "submit",
        })
        .then(function (token: string) {
          if (email) {
            axios
              .post("/api/waitlist", {
                email,
                captchaToken: token,
              })
              .then(() => {
								// success
              })
              .catch((err) => {
	              // error
              })
          }
        });
    });
  };
 
I used axios because it’s comfortable to use, but you can use fetch as well.
 

Protect your API route

At this point, we are only missing the backend api route (src/app/api/waitlist/route.ts)
TypeScript
import axios, { HttpStatusCode } from "axios";
import { NextResponse } from "next/server";
import qs from "qs";

export async function POST(req: Request) {
  const body = await req.json();
  const email = body.email;
  const captchaToken = body.captchaToken;

  if (!captchaToken) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: HttpStatusCode.Unauthorized }
    );
  }

  if (!email) {
    return NextResponse.json(
      { error: "Email is required" },
      { status: HttpStatusCode.BadRequest }
    );
  }

  const options = {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    data: qs.stringify({
      secret: process.env.RECAPTCHA_SECRET_KEY,
      response: captchaToken,
    }),
    url: "https://www.google.com/recaptcha/api/siteverify",
  };

  const response = await axios(options);

  if (response.data.success === false) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: HttpStatusCode.Unauthorized }
    );
  }
  
  // the captcha token is valid
}
 

Conclusion

Captchas are one of the most effective ways to protect a website, and the easiest solution to implement.
The captcha service from Google has improved a lot lately, and it doesn’t always require to solve a challenge to our users, which is perfect to provide them a great user experience.
I hope this was useful.
Cheers

Ship your startup in days and start selling.

Save weeks of coding with the #1 Next.js Startup and Chrome Extension Boilerplate. Made by a maker for makers.