How to Send Email Using Next.js (App Router), Nodemailer, React Hook Form, and Tailwind CSS Cover

How to Send Email Using Next.js (App Router), Nodemailer, React Hook Form, and Tailwind CSS

In modern web development, sending emails from a Next.js application using Nodemailer, React Hook Form, and Tailwind CSS can be a powerful combination. This article will guide you through the process step by step, ensuring you can implement this feature in your Next.js projects efficiently.

Prerequisites

Before diving into this tutorial, you should have a basic understanding of the following:

  • JavaScript (ES6+)
  • Node.js and npm (Node Package Manager)
  • React.js and Next.js basics
  • Familiarity with RESTful APIs

Repository Link for Assistance

In this repository, you will find the complete code showcasing how to send emails using Next.js, Nodemailer, React Hook Form, and Tailwind CSS. This comprehensive example provides a step-by-step guide and implementation of email functionality within a web application.

Feel free to explore the repository to understand the entire process and utilize the code as a reference for your own projects. Clone the repository to access the complete code and use it to integrate email sending capabilities seamlessly into your Next.js applications. Happy coding!

GitHub Repository

Setting Up the Project and Installing Dependencies

Before we dive into implementing email functionality with Next.js, Nodemailer, React Hook Form, Tailwind CSS, and react-toastify, follow these steps to set up your project:

  • Create a Next.js Project: Start by setting up a new Next.js project. Open your terminal and run the following commands.
npx create-next-app my-email-app cd my-email-app
  • Install Required Packages: Once your project is created, install the necessary packages:
npm install react-hook-form tailwindcss nodemailer react-toastify
  • react-hook-form: Used for managing form state and validation.
  • tailwindcss: A utility-first CSS framework for styling components.
  • nodemailer: A module for sending emails from Node.js applications.
  • react-toastify: A library for displaying toast notifications in React applications.

Configuring Environment Variables

We use environment variables to securely store our SMTP server credentials, such as the host, port, username, and password. Create a .env.local file in the root of your project and add the following variables:

SMTP_HOST="smtp.gmail.com"
SMTP_PORT="465"
SMTP_USER="abc@gmail.com"
SMTP_PASS="app password here"

Replace your_smtp_host, your_smtp_port, your_smtp_username, and your_smtp_password with your actual SMTP server details.

Creating the Email Form Component

We create a reusable ContactForm component using react-hook-form to handle form inputs and submission. This component renders an email form with input fields for recipient email, subject, and message.

"use client";

import { useState } from "react";
import { useForm } from "react-hook-form";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import clsx from "clsx";

export function ContactForm() {
  const [isLoading, setIsLoading] = useState(false);
  const {
    control,
    register,
    handleSubmit,
    watch,
    setValue,
    reset,
    formState: { errors },
  } = useForm({
    defaultValues: {},
  });
  const onSubmit = async (data) => {
    setIsLoading(true);
    fetch("/api", {
      method: "POST",
      headers: {
        Accept: "application/json, text/plain, */*",
        "Content-Type": "application/json",
        Authorization: "Q4n2ql2nqnZVlRlqwv",
      },
      body: JSON.stringify(data),
    })
      .then((res) => {
        setIsLoading(false);
        // console.log("Response received", res);
        if (res.status === 200) {
          // console.log("Response succeeded!");
          toast.success(
            "Thanks for submitting form. We will contact to you soon.",
            {
              position: "top-right",
              autoClose: 0,
              hideProgressBar: true,
            }
          );
        } else {
          setIsLoading(false);
          // console.log("Email/Password is invalid.");
          toast.error("Server Issue! please try again later", {
            position: "top-right",
            autoClose: 0,
            hideProgressBar: true,
          });
        }
      })
      .catch((error) => {
        setIsLoading(false); // Hide loading indicator on API call error
        console.error(error);
      });
    reset();
  };
  return (
    <>
      <div className="relative bg-white">
        <div className="lg:absolute lg:inset-0 lg:left-1/2">
          <img
            className="h-64 w-full bg-gray-50 object-cover sm:h-80 lg:absolute lg:h-full"
            src="https://images.unsplash.com/photo-1559136555-9303baea8ebd?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&crop=focalpoint&fp-x=.4&w=2560&h=3413&&q=80"
            alt=""
          />
        </div>
        <div className="pb-24 pt-16 sm:pb-32 sm:pt-24 lg:mx-auto lg:grid lg:max-w-7xl lg:grid-cols-2 lg:pt-32">
          <div className="px-6 lg:px-8">
            <div className="mx-auto max-w-xl lg:mx-0 lg:max-w-lg">
              <h2 className="text-3xl font-bold tracking-tight text-gray-900">
                Let's work together
              </h2>
              <p className="mt-2 text-lg leading-8 text-gray-600">
                Proin volutpat consequat porttitor cras nullam gravida at orci
                molestie a eu arcu sed ut tincidunt magna.
              </p>
              <form onSubmit={handleSubmit(onSubmit)} className="mt-16">
                <div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
                  <div>
                    <label
                      htmlFor="firstName"
                      className="block text-sm font-semibold leading-6 text-gray-900"
                    >
                      First name
                    </label>
                    <div className="mt-2.5">
                      <input
                        type="text"
                        name="firstName"
                        id="firstName"
                        {...register("firstName", { required: true })}
                      />
                    </div>
                  </div>
                  <div>
                    <label
                      htmlFor="lastName"
                      className="block text-sm font-semibold leading-6 text-gray-900"
                    >
                      Last name
                    </label>
                    <div className="mt-2.5">
                      <input
                        type="text"
                        name="lastName"
                        id="lastName"
                        {...register("lastName", { required: true })}
                      />
                    </div>
                  </div>
                  <div className="sm:col-span-2">
                    <label
                      htmlFor="email"
                      className="block text-sm font-semibold leading-6 text-gray-900"
                    >
                      Email
                    </label>
                    <div className="mt-2.5">
                      <input
                        id="email"
                        name="email"
                        type="email"
                        {...register("email", { required: true })}
                      />
                    </div>
                  </div>
                  <div className="sm:col-span-2">
                    <label
                      htmlFor="company"
                      className="block text-sm font-semibold leading-6 text-gray-900"
                    >
                      Company
                    </label>
                    <div className="mt-2.5">
                      <input
                        type="text"
                        name="company"
                        id="company"
                        {...register("company", { required: true })}
                      />
                    </div>
                  </div>
                  <div className="sm:col-span-2">
                    <div className="flex justify-between text-sm leading-6">
                      <label
                        htmlFor="phone"
                        className="block font-semibold text-gray-900"
                      >
                        Phone
                      </label>
                      <p id="phone-description" className="text-gray-400">
                        Optional
                      </p>
                    </div>
                    <div className="mt-2.5">
                      <input
                        type="tel"
                        name="phone"
                        id="phone"
                        {...register("phone", { required: true })}
                      />
                    </div>
                  </div>
                  <div className="sm:col-span-2">
                    <div className="flex justify-between text-sm leading-6">
                      <label
                        htmlFor="message"
                        className="block text-sm font-semibold leading-6 text-gray-900"
                      >
                        How can we help you?
                      </label>
                      <p id="message-description" className="text-gray-400">
                        Max 500 characters
                      </p>
                    </div>
                    <div className="mt-2.5">
                      <textarea
                        id="message"
                        name="message"
                        rows={4}
                        aria-describedby="message-description"
                        {...register("message", { required: true })}
                      />
                    </div>
                  </div>
                  <fieldset className="sm:col-span-2">
                    <legend className="block text-sm font-semibold leading-6 text-gray-900">
                      Expected budget
                    </legend>
                    <div className="mt-4 space-y-4 text-sm leading-6 text-gray-600">
                      <div className="flex gap-x-2.5">
                        <input
                          id="budget-under-25k"
                          name="budget"
                          defaultValue="under_25k"
                          type="radio"
                          {...register("budget", { required: true })}
                          className="mt-1"
                        />
                        <label htmlFor="budget-under-25k">Less than $25K</label>
                      </div>
                      <div className="flex gap-x-2.5">
                        <input
                          id="budget-25k-50k"
                          name="budget"
                          defaultValue="25k-50k"
                          type="radio"
                          {...register("budget", { required: true })}
                          className="mt-1"
                        />
                        <label htmlFor="budget-25k-50k">$25K – $50K</label>
                      </div>
                      <div className="flex gap-x-2.5">
                        <input
                          id="budget-50k-100k"
                          name="budget"
                          defaultValue="50k-100k"
                          type="radio"
                          {...register("budget", { required: true })}
                          className="mt-1"
                        />
                        <label htmlFor="budget-50k-100k">$50K – $100K</label>
                      </div>
                      <div className="flex gap-x-2.5">
                        <input
                          id="budget-over-100k"
                          name="budget"
                          defaultValue="over_100k"
                          type="radio"
                          {...register("budget", { required: true })}
                          className="mt-1"
                        />
                        <label htmlFor="budget-over-100k">$100K+</label>
                      </div>
                    </div>
                  </fieldset>
                </div>
                <div className="mt-10 flex justify-end border-t border-gray-900/10 pt-8">
                  <button
                    type="submit"
                    className={clsx(
                      "inline-flex justify-center items-center rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
                      isLoading && "cursor-not-allowed"
                    )}
                  >
                    {isLoading ? (
                      <>
                        <svg
                          className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
                          xmlns="http://www.w3.org/2000/svg"
                          fill="none"
                          viewBox="0 0 24 24"
                        >
                          <circle
                            className="opacity-25"
                            cx="12"
                            cy="12"
                            r="10"
                            stroke="currentColor"
                            strokeWidth="4"
                          ></circle>
                          <path
                            className="opacity-75"
                            fill="currentColor"
                            d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
                          ></path>
                        </svg>
                        Submitting...
                      </>
                    ) : (
                      "Submit"
                    )}
                  </button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
      <ToastContainer />
    </>
  );
}

Creating the API Route

Next.js allows us to create API routes for server-side functionality. Create a new file named app/api/route.js with the following content:

import { NextResponse } from "next/server";
import nodemailer from "nodemailer";

export async function POST(req) {
  try {
    const body = await req.json(); // Parse the JSON body
    // console.log("Parsed request body:", body);

    if (req.headers.get("authorization") !== "Q4n2ql2nqnZVlRlqwv") {
      return new Response(JSON.stringify({ status: "Unauthorized" }), {
        status: 400,
        headers: {
          "Content-Type": "application/json",
        },
      });
    }

    const transporter = nodemailer.createTransport({
      port: process.env.SMTP_PORT,
      host: process.env.SMTP_HOST,
      auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASS,
      },
      secure: true,
    });

    const mailData = {
      from: process.env.SMTP_USER,
      to: "jusmanasif435@gmail.com",
      //   bcc: "usmanasifdev@gmail.com",
      replyTo: body.email,
      subject: `New Contact at Usmanasifdev`,
      html: `
        <div><strong>Full Name:</strong> ${body.firstName}</div>
        <br/>
        <div><strong>Last Name:</strong> ${body.lastName}</div>
        <br/>
        <div><strong>Email Address:</strong> ${body.email}</div>
        <br/>
        <div><strong>Company:</strong> ${body.company}</div>
        <br/>
        <div><strong>Phone:</strong> ${body.phone}</div>
        <br/>
        <div><strong>How can we help you?:</strong> ${body.message}</div>
        <br/>
        <div><strong>Expected budget:</strong> ${body.budget}</div>
        <br/>
        <div>This Email was sent from a contact form on https://usmanasifdev.com/ </div>`,
    };

    await new Promise((resolve, reject) => {
      transporter.sendMail(mailData, function (err, info) {
        if (err) {
          console.error(err);
          reject(err);
        } else {
          console.log("Email sent:", info);
          resolve(info);
        }
      });
    });

    return new Response(JSON.stringify({ status: "OK" }), {
      status: 200,
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (err) {
    console.error("Error in processing request:", err);
    return new Response(
      JSON.stringify({ status: "Internal Server Error", error: err.message }),
      {
        status: 500,
        headers: {
          "Content-Type": "application/json",
        },
      }
    );
  }
}

Integrating the Email Form

Finally, integrate the ContactForm component into your main page. Open app/(main)/page.jsx and replace the default content with the following:

import { ContactForm } from "@/components/ContactForm";

export default function Home() {
  return (
    <>
      <ContactForm />
    </>
  );
}

Running the Application

Now you're ready to run your Next.js application:

npm run dev

Visit http://localhost:3000 in your browser, fill out the email form, and click "submit" to test the functionality.

Conclusion

Congratulations! You've successfully implemented email sending functionality in your Next.js application using Nodemailer, React Hook Form, and Tailwind CSS. Feel free to enhance the form with additional features such as attachments, HTML content, or error handling to suit your project's requirements.

GitHub Repository

If you have any questions or feedback, please don't hesitate to reach out. Happy coding!