Send an email with NextJS and SendGrid for free

Have you always wanted to send emails with JavaScript? Or more specifically, how to send emails with React or NextJS? Then let's find out together how to use SendGrid! It's a quick and easy solution to set up that will help us ensure that our emails are sent and, most importantly, received.
•
Updated on August 14, 2026
•
14 min read
Believemy logo

Have you always wanted to send emails with JavaScript? Or more specifically, how to send emails with React or even NextJS? Then let's discover together how to use SendGrid! It's an easy and quick solution to set up, which will ensure that our emails are sent and, most importantly, that they are successfully received.

 

Step 1: Create a NextJS Project

This step is perfectly optional. If you already have a NextJS project along with a perfectly functional form that correctly receives your users' data (contact form), you can skip to Step 4.

To understand how the email sending process works, we suggest starting with a NextJS project created from scratch.

In this project, we will add a contact page that will display a form where we can enter our first name, last name, email address, and the content of our message. You can then customize this project by reusing the same method we will explore together to send all kinds of emails, ranging from security notifications when a user logs in to sending a security code to finalize and confirm their registration.

To create a project with NextJS, let's start by using this command:

CONSOLE
npx create-next-app my-contact-form

This command will create a project named "My Contact Form" with all the necessary dependencies.

We will, of course, add more dependencies as we proceed with this mini-course.

 

Step 2: Create a Contact Page

To add a new page in a NextJS project, simply add a JavaScript file in the pages folder.

Since we want to add a contact page, we want this page to be accessible via /contact. Therefore, we will create the contact.js page in the pages folder.

You can also create a contact folder inside the pages folder and add an index.js file there. This method is valid if you prefer it.

Now, let's add the JSX that will allow us to display a nice contact form. Here's what the final code for the contact.js page will look like for now:

 

And for the CSS (in the globals.css file):

 

 

Step 3: Initialize React Hook Form

We have just added a new page and placed a contact form on it: we can be proud of the progress we've made so far.

The problem is as follows: our form isn't working. And that's rather annoying.

What we mean is that the form isn't sending data to NextJS—or rather, we aren't retrieving the submitted data.

To do this, we will use the react-hook-form library. This excellent library is one of the best ways to handle React and NextJS forms currently. There are, of course, many other possibilities, and you are free to use the method that suits you best.

Let's start by installing React Hook Form:

CONSOLE
npm install react-hook-form

Now, let's go to our contact.js page and add this new dependency at the top of the file:

JSX
import { useForm } from 'react-hook-form';

We can now begin to retrieve the data from our form!

 

Step 4: Display the Received Data

To display the data we receive, we need to start by setting up React Hook Form to tell it, "I'm giving you control of the first name field," then "last name," "email address," and finally "content."

To achieve this, we will need to use several variables that are returned by React Hook Form. Here's how we'll do it.

JSX
// Library
import { useForm } from 'react-hook-form';

export default function Contact() {
    // Variables
    const { register, handleSubmit, formState: { errors } } = useForm();

    // JSX
    return ...
}

All these variables are retrieved from useForm() and have their uses:

  • register - allows you to register a form field with React Hook Form
  • handleSubmit - allows you to retrieve all the form data and send it to a very specific method
  • errors - allows you to know when a form field is invalid

Of course, we're not here to give a mini-course on React Hook Form, but here are some basic elements that can be interesting when you're not familiar with this library.

So, we have the necessary variables for React Hook Form to function correctly. Now, let's add our form fields! Here's what we have so far for the prenom (first name) field:

JSX
<input className="input" placeholder="Email Address" />

Here's what we will have thanks to React Hook Form:

JSX
<input
    className="input"
    placeholder="Email Address"
    {...register('prenom', { required: true }) }
/>

Here, we register the form field with the identifier prenom for React Hook Form and add a validation rule to make the field required each time. Here's what our entire form will look like:

 

I now propose to retrieve all this information when our form is submitted.

Here's some good news: it's very easy to set up thanks to React Hook Form!

We will add the onSubmit event to the <form> tag of our form and tell it to call the handleSubmit method provided by React Hook Form.

JSX
<form onSubmit={handleSubmit()}>

Don't forget that this method allows you to retrieve all the form information and send it to a very specific method: our own method! Let's say we will have an onSubmitHandler method, so let's specify it in handleSubmit right away.

JSX
<form onSubmit={handleSubmit(onSubmitHandler)}>

All that's left is to display our information by creating the onSubmitHandler method in our contact page.

JSX
// Library
import { useForm } from 'react-hook-form';

export default function Contact() {
    // Variables
    const { register, handleSubmit, formState: { errors } } = useForm();

    // Method
    const onSubmitHandler = data => {
        console.log(data);
    }

    // JSX
    return ...
}

Let's add information, submit the form... Our data is correctly displayed in the console! Wonderful.

You might know our reputation: we always go in-depth, even when it's just content provided for free. So, we will show you how to display a message to your users when they don't fill in all the information.

Specifically, when a form field is empty, React Hook Form highlights it when the user tries to submit the form. But displaying a small message asking them to fill in the field is much better.

Here's how we can do this (don't forget our errors variable that we added):

 

With just this code, when our user doesn't fill in their first name, a small message will appear and disappear on its own like magic once they've added or haven't added a value. Magical, isn't it?

 

Step 5: Create an Account on SendGrid

We now have a contact form that is perfectly functional, so all that's left is to send this information to send an email. And for that, we obviously need SendGrid in this mini-course.

You can create an account on SendGrid by going here.

Creating an account is free; you will only pay if you wish to send more than 100 emails per day (which is extremely generous).

Dashboard in SendGrid
Dashboard

 

Step 6: Create a Template

We now want to go to the Email API category and finally to Dynamic Templates.

Dynamic templates are emails that you can personalize with very specific information. Thus, we will set up the design of our email and fill it with the information provided by our user at the time of form submission: SendGrid is magic! (SendGrid is magic! - Yes, we're thinking of you who don't like English))

We will click on Create a Dynamic Template - the big blue button on the page - and give our template a name. For example, contact.

You should now see the Contact template on your page. Click on it and then click on Add Version.

We can now choose between two options:

  • Add a design (Your email design);
  • Use a SendGrid template (SendGrid Email Designs).

You can use whichever one you prefer. Know that we recommend using a SendGrid template to start with. Here's the one we will choose:

Example

We recommend using the designer mode and not the code mode. It's simpler to navigate the template this way; you can always modify the HTML when we need to.

So, try to customize the template according to your tastes. If you have trouble, you can always check out our YouTube video on this subject.

Here's what we've done:

Example

We will now make the template dynamic. The goal is to have a personalized message with the information provided by each user, so we expect to have:

  • firstName;
  • lastName;
  • content;
  • email.

Without capital letters and without any special characters.

What is very simple with SendGrid is that to add dynamic information, you just need to go into the HTML and replace the text with:

MARKDOWN
{{ prenom }}

To display the first name dynamically. And if you want to allow HTML, just add another set of curly braces around it.

MARKDOWN
{{{ contenu }}}

As you have probably understood, try to add all dynamic information to your template so that each sent message is unique.

You can test your variables using the preview mode (click on Preview). Then you can click on the "Add Test Data" button and enter some JSON. Here's an example of JSON to test your template on SendGrid with the variables.

JSON
{
 "contenu": "I would like to create a website for my new company SpaceX. Could you provide information about the Rocket Program? I've heard a lot of good things.",
 "prenom": "Elon",
 "nom": "Musk",
 "email": "elon@spacexploration.com"
}

 

Step 7: Create an API on NextJS

The contact form is functional, and our template on SendGrid is too; we just need to send the information to have the email sent. To do this, we will create an API using the system provided by NextJS.

Simply create an api folder inside the pages folder and add a file there, for example, contact.js. Everything written in this file will remain hidden from our user's eyes. We can add our SendGrid login credentials here, for example. All the code will be executed on the server, not in the browser. This is one of the big advantages provided by NextJS. Here's what our API will look like:

  • Verification of the used method;
  • Verification of the email address syntax;
  • A request to send the information to SendGrid;
  • Verification to return a success or failure of the sending process.

Of course, you can create all this yourself; that's actually what we would like you to do: there's no better way to learn than by trying. Nevertheless, here's what we have:

JSX
// Library
import sgMail from "@sendgrid/mail";

export default function handler(req, res) {
	if (req.method !== "POST") {
		res.status(405).json({ message: "INVALID_METHOD" });
		return;
	}

	// Variables
	const { prenom, nom, email, contenu } = req.body;

	if (!prenom || !nom || !email || !contenu) {
		res.status(400).json({ message: "INVALID_PARAMETER" });
		return;
	}

	// Email address syntax
	const pattern =
		/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
	if (!pattern.test(email)) {
		res.status(400).send({
			message: "EMAIL_SYNTAX_INCORRECT",
		});
		return;
	}

	// Transform line breaks for HTML
	const message = contenu
		.replace(/\n/g, "<br>")
		.replace(/\r/g, "<br>")
		.replace(/\t/g, "<br>")
		.replace(/<(?!br\s*\/?)[^>]+>/g, ""); // removes all HTML except 
 tags

	// Provide the API key
	sgMail.setApiKey(process.env.KEY_SENDGRID);

	// Create the message
	const sendGridMail = {
		to: "hello@believemy.com",
		from: "louisnicolas@believemy.com",
		templateId: "d-98ac3ceded9745448d3f4522a0f3eb26",
		dynamic_template_data: {
			prenom: prenom,
			nom: nom,
			email: email,
			contenu: message,
		},
	};
	// SendGrid
	(async () => {
		try {
			await sgMail.send(sendGridMail);
			res.status(200).json({
				message: "EMAIL_SENT_SUCCESSFULLY",
			});
		} catch {
			res.status(500).json({
				message: "ERROR_WITH_SENDGRID",
			});
			return;
		}
	})();
}

As you can see, nothing too new if you're already familiar with NextJS. If not, a quick tour of our comprehensive NextJS course is highly recommended. We have also used an environment variable (process.env.KEY_SENDGRID). To add an environment variable, go to the next.config.js file and add this:

JAVASCRIPT
module.exports = {
	reactStrictMode: true,
	env: {
		KEY_SENDGRID:
			"YOUR_API_KEY",
	},
};

Your key to allow you to make requests to SendGrid's API can be found in your account, under settings and API keys. Simply create a new one with all permissions.

 

Step 8: Call the API on the Client

Our API works; all that's left is to use it!

To call it, we need a trigger, a moment when we know our form is submitted would be perfect! Upon some reflection, we have precisely this trigger: the onSubmitHandler method.

Here's what we could do:

JSX
const onSubmitHandler = async (data) => {
    const response = await fetch("/api/contact", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
    });

    const result = await response.json();
    
    if (!response.ok) {
        console.log("error");
    } else {
        console.log("ok");
    }
};

Don't forget to add a little message during the sending process so that your user doesn't wait while the form is processing. For this, we can use a state with useState. Let's call it isLoading.

JSX
// Libraries
// ...
import { useState } from 'react';

export default function Contact() {
    // Variables
    // ...

    // State
    const [isLoading, setIsLoading] = useState(false);

    // ...
}

Let's modify the state when we use the API we designed and add a condition to prevent sending multiple requests at the same time:

JSX
const onSubmitHandler = async (data) => {
    if (!isLoading) {
        setIsLoading(true);

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

        const result = await response.json();

        setIsLoading(false);

        if (!response.ok) {
            console.log("error");
        } else {
            console.log("ok");
        }
    }
};

If we don't perform any checks, a user who clicks the "Send" button 15 times would have sent the same email 15 times! Always think to check if the form is not already in the process of being sent.

Finally, all that's left is to clear the form and display a small message once the email is successfully received by SendGrid. Here's what we'll do to clear the form:

JSX
// Variables
const {
    register,
    handleSubmit,
    reset, // we add the reset method
    formState: { errors },
} = useForm();

// State
const [isLoading, setIsLoading] = useState(false);

// Method
const onSubmitHandler = async (data) => {
    if (!isLoading) {
        setIsLoading(true);

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

        const result = await response.json();

        setIsLoading(false);

        if (!response.ok) {
            console.log("error");
        } else {
            console.log("ok");
            reset(); // here
        }
    }
};

Very easy, isn't it? We just need to add the success message, and everything will be perfectly functional. We recommend using a new state isSended.

JSX
// Libraries
// ...
import { useState } from 'react';

export default function Contact() {
    // Variables
    // ...

    // States
    const [isLoading, setIsLoading] = useState(false);
    const [isSended, setIsSended] = useState(true);

    // ...
}

Here's what we have now in onSubmitHandler:

JSX
const onSubmitHandler = async (data) => {
    if (!isLoading) {
        setIsLoading(true);

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

        const result = await response.json();

        setIsLoading(false);

        if (!response.ok) {
            console.log("error");
        } else {
            console.log("ok");
            reset();
            setIsSended(true);
        }
    }
};

All that's left is to display the success message above our form when the isSended state changes and is true.

JSX
// The rest of the code

<form
    style={{ width: "500px", margin: "auto" }}
    onSubmit={handleSubmit(onSubmitHandler)}
>
    {isSended && (
        <p>
            Your message has been successfully sent. We will respond to you quickly.
        </p>
    )}
    <div
        style={{
            backgroundColor: "#f5f5f5",
            padding: "30px",
            borderRadius: "5px",
            textAlign: "left",
        }}
    >

    // The end of the code

And there you have it! We now have a very well-designed contact form, perfectly usable and functioning thanks to SendGrid, ensuring that all emails are properly sent to the destination.

All that's left is to reuse this same model to send other emails on other occasions if you wish. Just create a template—whether dynamic or not—and send a request to SendGrid.

If you want to learn more about NextJS, we have a comprehensive course dedicated to this subject. Also, feel free to share this article with your network on your social media. Thank you!

Feel free to download the project source code here.

Category: Developers

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.

Believemy logo
Comments (0)

You need to be logged in to comment on this article: log in or sign up.

Read next

How to use Server Actions with NextJS

Discover why and how NextJS Server Actions improve performance on both Client Components and Server Components, and learn how to use them in your projects.

December 9, 2024

Understanding .env files with ReactJS

Find out how to really use the .env file with React or NextJS so you never make a mistake again.

December 9, 2024

Implementing over-request revalidation with NextJS

If you're a web developer, you probably know how important it is to have a fast, responsive site to deliver an optimal user experience. In this article, we're going to show you how to set up on-demand revalidation with NextJS, a feature that refreshes your site's data only when necessary. Using this method, you can save resources and improve the performance of your website, while offering up-to-date data to your users.

December 9, 2024

React vs NextJS: what should you choose for your project in 2025?

Which technology should you use, React or NextJS? Discover their advantages and disadvantages to choose the one best suited to your projects.

December 9, 2024

How to install TinyMCE in a project using React or NextJS?

Find out how to install TinyMCE in React or NextJS and fully customize it with plugins to match your projects.

December 6, 2024

How to use localStorage in a NextJs or React application

The aim of this article is to teach you how to use localStorage in a Next.JS or React application. Indeed, there are a few differences with the normal use, in a vanilla application.

December 11, 2024

Setting up a loader when changing a Nextjs page

Have you ever been on a site where you click on a link and nothing happens? Or perhaps you've wondered whether your request was actually received? This is where having a loader comes in. In this article, we'll explain why it's important to have a loader on your website, how to set it up in your Nextjs project and create a function that will display the loader until the page is loaded. If you want to improve the user experience on your website and reassure your visitors, this article is for you.

December 11, 2024

Setting up SCSS themes and contexts with NextJs or React

You can't modify a scss variable, because scss converts its code to css, applying the variable values directly. This makes it difficult to set up a light/dark theme on your React or Next project and scss. Fortunately, there are solutions, which we'll be exploring in this article.

December 11, 2024

Become the solopreneur you dream of being.

First project, career change, business launch: access our courses, practice on real-world cases, and move forward with clear guidance.