Integrating Recaptcha in Nextjs Site

» What is reCaptcha?

reCaptcha

  1. reCAPTCHA is a free service provided by Google that helps protect websites from spam and abuse.

  2. According to Copilot: It uses an advanced risk analysis engine and adaptive challenges to keep automated software from engaging in abusive activities on your site. It does this while letting your valid users pass through with ease.

» What we building?

reCaptcha

  1. We will build the form(shown above) where users can register themselves but before that they have to confirm that they are not the bot by clicking on this ā€œIm not a robotā€ checkbox. And believe me itā€™s really simple to do that. So let’s go.

  2. Also reCaptcha only shows challenges(puzzle to solve) if user fails its behaviour or cookie analysis.

» Let’s build the form

  1. Open the terminal and letā€™s create a next.js app by running the command:
pnpm create-next-app
  1. Say no for typescript, src dir, tailwind css and also no for router, if you want to use tailwind select yes. But for this we will use simple css.

  2. Now letā€™s open this project into vs code and now letā€™s now head to reCAPTCHA admin console

reCaptcha

  1. And then letā€™s provide the label for our new site, letā€™s say nextjs-recaptcha-app and then select version two(v2) challenge and in that “I’m not a robot” checkbox. V3 is score based and it does not interrupt with users but does check for any bot behaviour. Then provide the domain as “localhost”, if you have your site published then provide that domain name here with localhost by clicking on the plus icon.

reCaptcha

» Creating our form

  1. And then after clicking on submit we will have the site and secret key. Letā€™s work with them now. Get back to vs code and in the root directory create a .env file and inside this file letā€™s bring in our site key and a secret key. Just like this:
//.env file
# Add the public site key here
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
# Add the secret key here
RECAPTCHA_SECRET_KEY=
  1. We need this site key to be available to the client so for that we use this site key with the suffix NEXT_PUBLIC and because of it this site key will be available to the client. We wonā€™t do the same thing for secret key because we donā€™t want it exposed to the client. If you donā€™t want to do it like this you can also create various environment for your environment variables like .env.production or .env.local. Its really up to you.

  2. Now inside the pages directory we have a file named index.js remove all of its content and then letā€™s start by creating our form.

  3. But before that letā€™s run the command:

pnpm i react-google-recaptcha axios node-fetch
  1. If you donā€™t use pnpm, its okay, you can use npm or yarn instead of pnpm.

  2. And then back to the index.js file we will start off by importing react from react, also letā€™s import head from next/head and Recaptcha from the package we just installed that is react-google-recaptcha

// pages/index.js
import React from "react";
import Head from "next/head";
import ReCAPTCHA from "react-google-recaptcha";
  1. Now letā€™s declare the export default function home and inside it we start off with the state initialization using React Hook useState. So, letā€™s first import useState hook from react at top of the file:
// pages/index.js
import React from "react";
// ....
import { useState, useRef } from "react";
  1. And then inside the home function letā€™s declare three state variables: email, name, and captchaValue. These are going to hold the user’s email, name, and the value of the reCAPTCHA.
// pages/index.js
// ...
export default function Home() {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [captchaValue, setCaptchaValue] = useState(null);

  const recaptchaRef = useRef(null);
}
  1. Then we also want to use the useRef hook, so letā€™s first import it first beside the useState hook. We use useRef to create a reference to the reCAPTCHA component.
//index.js
// ...
const recaptchaRef = useRef(null);
  1. Then inside the return statement we are going to render our form. So letā€™s start with that.
// index.js
// ...
return (
  <div className="container">
    <Head>
      <title>Recaptcha with Next</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>
    <div id="newsletter-form">
      <h2 className="header">Subscribe to our Newsletter.āœØ</h2>
      <div>
        <form onSubmit={handleSubmit}>
          <label htmlFor="name">Name</label>
          <input
            onChange={handleChange}
            required
            type="text" // Change type to 'text' for the name input
            name="name"
            value={name}
            placeholder="John Doe"
          />
          <label htmlFor="email">Email</label>
          <input
            onChange={handleChange}
            required
            type="email"
            name="email"
            value={email}
            placeholder="[email protected]"
          />
          <button type="submit">Register</button>
        </form>

        <ReCAPTCHA
          ref={recaptchaRef}
          sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}
          onChange={handleCaptchaChange}
        />
      </div>
    </div>
  </div>
);
  1. Create a div with the className container inside it we have another div with id newsletter-form and inside it we will render a h2 heading which says ā€œsubscribe to our newsletterā€.

  2. Then we have a form element inside which we will have two another elements label and input for Name. Just like this we will have another label and input elements for email.

  3. The form element also had this onSubmit event handler that calls the handleSubmit function when the form is submitted. It is essential for connecting the form submission logic to the handleSubmit function.

  4. Outside the form we have The reCAPTCHA component which is going to provide the ā€œIā€™m not a robotā€ checkbox. Then we use of the recaptchaRef to reset the reCAPTCHA after a successful submission and then we will have a site key attribute which takes the key from the .env file and we also have a onChange attribute which listens for the change event and calls handleCaptchaChange with the new value of the reCAPTCHA response token when the user completes the reCAPTCHA verification.

  5. This handleChange, handleSubmit and handleCaptchaChange event handler function we are going to create them just now.

  6. So below the recaptcharef variable letā€™s declare the handleChange function. This function is going to update the state based on user input for name and email. This function will be called when the user types in the input fields to update the state with the new value that the user has inserted.

// index.js
// ..
const handleChange = ({ target: { name, value } }) => {
  if (name === "name") {
    setName(value);
  } else if (name === "email") {
    setEmail(value);
  }
};
  1. Then we declare the handleCaptchaChange function that is going to update the state with the reCAPTCHA value. This function will be called when the user completes the reCAPTCHA verification process to get the token value so that it can be sent to the server.
// index.js
// ..
const handleChange = ({ target: { name, value } }) => {
  if (name === "name") {
    setName(value);
  } else if (name === "email") {
    setEmail(value);
  }
};

// this added
const handleCaptchaChange = (value) => {
  setCaptchaValue(value);
};
  1. Now here comes the boss, handleSubmit asynchronous function. handleSubmit function is called when the user submits the form.
// index.js
// ...
const handleSubmit = async (event) => {
  event.preventDefault();

  try {
    const response = await fetch("/api/register", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name,
        email,
        captcha: captchaValue,
      }),
    });

    if (response.ok) {
      window.location.href = "/profile";
    } else {
      const data = await response.json();
      alert(data.message);
    }
  } catch (error) {
    console.error("Error submitting form:", error);
    alert("Something went wrong. Please try again later.");
  }
};
  1. event.preventDefault() will help prevent the default form submission behaviour, which would cause the page to reload.

  2. We then declare the try and catch statement. Inside the try statement we will send a POST request to the server with the user’s name, email, and reCAPTCHA token and If the request is successful, the user will be redirected to the profile page. Or else we say If the request fails, an alert will be shown with the error message Something went wrong.

  3. If everything checks out, the function is going to sends a POST request to the “/api/register” endpoint with the user’s data.

  4. We are yet to create this register api endpoint and profile page, so donā€™t worry.

  5. For now letā€™s save the file and open the terminal and run the command

pnpm run dev 

to check what we have till here, so the site has started on the localhost 3000. And not going to lie it does not look nice without the css, so letā€™s beautify this.

» Full index.js file code

import React from "react";
import Head from "next/head";
import ReCAPTCHA from "react-google-recaptcha";
import { useState, useRef } from "react";

export default function Home() {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [captchaValue, setCaptchaValue] = useState(null);

  const recaptchaRef = useRef(null);

  const handleChange = ({ target: { name, value } }) => {
    if (name === "name") {
      setName(value);
    } else if (name === "email") {
      setEmail(value);
    }
  }; 

  const handleCaptchaChange = (value) => {
    setCaptchaValue(value);
  }; 
  const handleSubmit = async (event) => {
    event.preventDefault(); 

    try {
      const response = await fetch("/api/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          name,
          email,
          captcha: captchaValue,
        }),
      });

      if (response.ok) {
          window.location.href = "/profile";
      } else {
        const data = await response.json();
        alert(data.message);
      }
    } catch (error) {
      console.error("Error submitting form:", error);
      alert("Something went wrong. Please try again later.");
    }
  };

  return (
    <div className="container">
      <Head>
        <title>Recaptcha with Next</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div id="newsletter-form">
        <h2 className="header">Subscribe to our Newsletter.āœØ</h2>
        <div>
          <form onSubmit={handleSubmit}> 
            <label htmlFor="name">Name</label>
            <input
              onChange={handleChange}
              required
              type="text" 
              name="name"
              value={name}
              placeholder="John Doe"
            />
            <label htmlFor="email">Email</label>
            <input
              onChange={handleChange}
              required
              type="email"
              name="email"
              value={email}
              placeholder="[email protected]"
            />
            <button type="submit">Register</button>
          </form>

          <ReCAPTCHA
              ref={recaptchaRef}
              sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}
              onChange={handleCaptchaChange}
            />
        </div>
      </div>
    </div>
  );
}

» Styling the form

  1. Open the global.css file which is present in this styles folder, inside this file letā€™s remove all of the styles and replace it with this
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}  

body {
  background-color: #eee;
  font-family: 'helvetica neue', helvetica, arial, sans-serif;
  color: #222;
}

.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #191919;
}

#newsletter-form {
  width: auto;
  margin: 0 auto;
  background-color: #fcfcfc;
  padding: 20px 50px 40px;
  box-shadow: 1px 4px 10px 1px #aaa;
  font-family: sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
}

#newsletter-form * {
    box-sizing: border-box;
}

#newsletter-form h2{
  text-align: center;
  margin-bottom: 30px;
}

#newsletter-form label {
  font-weight: bold;
  color: #777;
}

#newsletter-form input {
  display: block;
  height: 32px;
  padding: 6px 16px;
  width: 100%;
  border: none;
  background-color: #aaaaaa72;
  width: 300px;
  height: 40px;
  margin-top: 15px;
  margin-bottom: 15px;
  border-radius: 4px;
}

#newsletter-form button[type=submit] {
  display: block;
  margin: 20px auto 0;
  width: 300px;
  height: 40px;
  border-radius: 4px;
  border: none;
  color: #eee;
  font-weight: 700;
  box-shadow: 1px 4px 10px 1px #aaa;
  margin-bottom: 20px;
  background: #333333;
  cursor: pointer;
}
  1. Now save the file and there we have our form styled perfectly.

» Creating an API Endpoint

  1. Now letā€™s create our API endpoint register. To do that letā€™s create a file named register.js inside the API folder. And in this file we start by importing fetch from node fetch package which we installed earlier. This is a library that allows us to make HTTP requests from the server.
// api/register.js 
import fetch from "node-fetch";
  1. Then we create a sleep arrow function that will be used to simulate a delay in the server response to test the loading state of the form. Inside the arrow function, we create a new Promise.
const sleep = () => new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 350);
});
  1. Within the Promise constructor, we will have a setTimeout function. This function sets a timer and, after a 350 milliseconds, itā€™s going to call the resolve function which we will create just now.

  2. Letā€™s now declare a export default async function handler that takes request and response as an parameters.

  3. Inside the function we begin by extracting the request body and method.

export default async function handler(req, res) {
const { body, method } = req;
}
  1. Then we will destructure the body object to get the name, email, and captcha fields.
const { name, email, captcha } = body;
  1. šŸšØ Now, this is where we need to pay attention to the method check.
// ...
  if (method === "POST") {
    
    if (!email || !captcha || !name) {
      return res.status(422).json({
        message: "Unproccesable request, please provide the required fields",
      });
    }
  }

  1. Method check is going to ensures that the function only proceeds if the incoming request is a POST request.

  2. Why is this important? Well, we want to make sure that we’re handling registration data only when a user is submitting a form.

  3. Then we also check if the required fields that is name, email, and captcha are present in the request body.

  4. If any of them are missing, we will responds with a 422 status code, indicating an unprocessable request and will users to fill in the required fields

  5. šŸŒ Now, this is where the exciting part comes in!

// inside the if statement
 try {
      const response = await fetch(
        `https://www.google.com/recaptcha/api/siteverify?secret=
        ${process.env.RECAPTCHA_SECRET_KEY}&response=${captcha}`,
        {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
          },
          method: "POST",
        }
      );
      const captchaValidation = await response.json();

      if (captchaValidation.success) {

        await sleep();

        return res.status(200).send("OK");
      }

      return res.status(422).json({
        message: "Unproccesable request, Invalid captcha code",
      });
    } catch (error) {
      console.log(error);
      return res.status(422).json({ message: "Something went wrong" });
    }
  1. Our code is going to interacts with Google’s reCAPTCHA API. So letā€™s create a try and catch statement and inside the try statement we are going to send a request to verify the captcha value received from the client.

  2. This is the request to the Google reCAPTCHA API to validate the token. We are using the fetch function to make the request. We also pass the secret key and the token as query parameters in the URL.

  3. We are also setting the Content-Type header to application/x-www-form-urlencoded; charset=utf-8 and the method to POST.

  4. captchaValidation is the response from the Google reCAPTCHA API. We are parsing the response to JSON format to get the success and error-codes fields. we are using the await keyword to wait for the response to be parsed before continuing.

  5. šŸ¤– Then we check that the captchaValidation that ensures that yes, the captcha was valid. If successful, we do a short delay and its because of the sleep function we created above, and then we responds with a 200 status, indicating success.

  6. šŸš« And If by any chance the captcha validation fails, we will respond with a 422 status and an error message.

  7. ERROR HANDLING šŸ›: And then finally inside the catch block we handle any unexpected errors that might occur during this process, we log the error and respond with a 422 status. And a message that says ā€˜Something went wrongā€™

  8. And If the request method is not POST, we are going to return a 404 status code with the message Not found.

// outside the if statement
return res.status(404).send("Not found");
  1. And thatā€™s all for the server side code. Now letā€™s save the file.

» Full server side code

  1. api/register.js file
import fetch from "node-fetch"; 

const sleep = () => new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 350);
});

export default async function handler(req, res) {
  const { body, method } = req;

  const { name, email, captcha } = body;

  if (method === "POST") {
    
    if (!email || !captcha || !name) {
      return res.status(422).json({
        message: "Unproccesable request, please provide the required fields",
      });
    }

    try {
      const response = await fetch(
        `https://www.google.com/recaptcha/api/siteverify?secret=
        ${process.env.RECAPTCHA_SECRET_KEY}&response=${captcha}`,
        {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
          },
          method: "POST",
        }
      ); 
      const captchaValidation = await response.json(); 

      if (captchaValidation.success) {

        await sleep();

        return res.status(200).send("OK");
      }

      return res.status(422).json({
        message: "Unproccesable request, Invalid captcha code",
      });
    } catch (error) {
      console.log(error);
      return res.status(422).json({ message: "Something went wrong" });
    }
  }

  return res.status(404).send("Not found"); 
}

» Create the profile page

  1. One last thing we are yet to do is to create the profile page so letā€™s create a profile.js file inside the pages directory and in this file lets declare the export default function Profile and in return statement we just return a heading Profile.
// profile.js
export default function Profile() {
  return (
    <div>
      <h1>Profile</h1>
    </div>
  );
}
  1. Save the file and then restart the server by running the command
pnpm run dev 
  1. and letā€™s reload the site and try submitting this form. Fill the inputs and then click the Iā€™m not a robot checkbox and there it does takes us to the profile page.

reCaptcha

  1. And now if you want to check that it really does gives user to solve the puzzle when they seems suspicious, lets do one thing, letā€™s head back to the recaptcha site and there in the settings increase the security to max and then letā€™s start the incognito tab and go to localhost 3000 and here letā€™s try submitting the form. Look carefully when we hit the Iā€™m not a robot checkbox it gave us the puzzle to solve. So solve that and there it took us to the profile page after it confirmed that we are not the robot.

So thatā€™s how we add reCaptcha to our Next.js site. But do remember this is just a layer of protection, you can make it more procted by using authentication/authorisation like NextAuth etc.

šŸ‘‰ I hope this helped you. And I’ll see you in next blog till then bye bye.