There are many paid form handling API services out there. Some service have free-tier accounts, but are very limited. But who wants to pay for the simple function of sending a form to an email address?

If all you want to do is collect information from a website user and email it to yourself and also send a confirmation email to the user, and you don't need any database storage, then this tutorial is for you!

This tutorial is part two of a series on creating web forms using Next.JS, React-Hook-Form, Yup and Bootstrap. You'll want to read the first article, Next.JS - Kick-Ass Form Handling Using React-Hook-Form, Yup and Bootstrap, before proceeding.

Step 1 - Set Up Our Environment

First we need to add a couple additional packages to our project to handle the back-end of our form.

npm install nodemailer axios

Now, to use your Gmail account as a form mailer, you'll need to log in to your Google account and set up an App Password for the back-end API to use. If you're unsure how to do this, check out This Tutorial.

Next, let's set up our local dev environment. Create a file in the root directory called .env.local:

[email protected]

Step 2 - Create Our Back-End Form Handler

Ok, the first thing we need to do is create a back-end API endpoint to handle our form data and send it to our gmail account.

Create a folder in the public folder called email-templates. These templates will be used by the API backend. Each email will have an HTML and Plaintext version.


This file will be used by all email templates.

<!DOCTYPE html>
    <link rel="preconnect" href="">
    <link rel="preconnect" href="" crossorigin>
    <link href=";600&display=swap" rel="stylesheet">
        body {
            background-color: #171717;
            color: #d8d8d8;
            font-family: 'Montserrat', sans-serif;
            padding: 1em;


<h1>New Message From Website</h1>
<p>The following information was submitted:</p>
<p>Name: %NAME%</p>
<p>Email: %EMAIL%</p>


New Message From Website
The following information was submitted:
Name: %NAME%
Email: %EMAIL%


<h1>We Received Your Message!</h1>
<p>Dear %NAME%:</p>
<p>Thank you for inquiring. We just wanted to let you know that we've received your message and will be responding soon.
<p>Have a great day!</p>
<p>The Webmaster</p>


Message Received!
Dear %NAME%:
Thank you for inquiring. We just wanted to let you know that we've received your message and will be responding soon.
Have a great day!
- The Webmaster

Now let's create a file called contact-form.js in the /pages/api/ directory:

import axios from 'axios';
const nodemailer = require("nodemailer");
// Config
const mailConfig = {
    host: "",
    port: 465, // or 587
    secure: true, // true for 465, false for other ports
    auth: {
        user: process.env.NEXT_PUBLIC_GMAIL_USER, // your gmail account
        pass: process.env.NEXT_PUBLIC_GMAIL_PASS // your gmail app password
const adminEmail = 'The Webmaster <[email protected]>';
// Function for grabbing template files
async function getPubFile(file) {
    const res = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}${file}`);
export default async function handler(req, res) {
    sendEmails(req, res);
async function sendEmails(req, res) {
    // Create our Nodemailer transport handler
    let transporter = nodemailer.createTransport(mailConfig);
    // Fetch our template files
    const template = await getPubFile("/email-templates/template.html");
    const custHtml = await getPubFile("/email-templates/customer.html");
    const adminHtml = await getPubFile("/email-templates/admin.html");
    const custTxt = await getPubFile("/email-templates/customer.txt");
    const adminTxt = await getPubFile("/email-templates/admin.txt");
    // Format our recipient email address
    const recipEmail = `${} <${}>`;
    // Format our customer-bound email from received form data
    let sendHtml = template.replace("%BODY%", custHtml)
        .replace("%MESSAGE%", req.body.message);
    let sendTxt = custTxt
        .replace("%MESSAGE%", req.body.message);
    // Send our customer-bound email
    let info = await transporter.sendMail({
        from: adminEmail,
        to: recipEmail, // list of receivers
        subject: "Message Received ✔", // Subject line
        text: sendTxt, // plain text body
        html: sendHtml, // html body
    if (!info.messageId) {
        res.status(200).json({ status: 0, message: "Failed to send message!" });
        return false;
    sendHtml = template.replace("%BODY%", adminHtml)
        .replace("%MESSAGE%", req.body.message);
    sendTxt = adminTxt
        .replace("%MESSAGE%", req.body.message);
    info = await transporter.sendMail({
        from: recipEmail,
        to: adminEmail, // list of receivers
        subject: req.body.subject ? req.body.subject : "New Message From Website ✔", // Subject line
        text: sendTxt, // plain text body
        html: sendHtml, // html body
    if (info.messageId) {
        res.status(200).json({ status: 1 });
    } else {
        res.status(200).json({ status: 0, message: "Failed to send message!" });

Step 3 - Modify Our Front-End Form to Submit to Our API

Assuming you have completed the first tutorial, edit the /pages/index.js to look like this:

import React from 'react';
import Link from 'next/link';
import { Col, Container, Row, Navbar, Form, Button } from 'react-bootstrap';
import { useForm } from 'react-hook-form';
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import axios from 'axios';
export default function Home() {
  // Initialize our states
  const [isSubmitting, setIsSubmitting] = React.useState(false);
  const [isSubmitted, setIsSubmitted] = React.useState(false);
  // Yup error message overrides
  const errMess = {
    req: "Please fill this out"
  // Our Yup Schema for this form
  const ContactSchema = yup.object().shape({
    name: yup.string()
      .label('Full Name')
    email: yup.string()
      .label('Email Address')
      .email('Invalid Email Address'),
    message: yup.string()
  // Destruct useForm() and set our Yup schema as the validation resolver
  const {
    formState: { errors },
  } = useForm({
    resolver: yupResolver(ContactSchema)
  // Send our valid form data to our back-end API
  const submitForm = async (data) => {
    const res = await axios({
      method: 'POST',
      url: '/api/contact-form',
      data: data
    }).then((res) => {
      return res;
    }).catch((e) => {
      alert("An error occurred. See log for details.")
    if ( === 1) {
    } else {
  return (
      <Navbar bg="dark" expand="lg">
          <Link href="/">
        {!isSubmitted ?
            <h1 className='mb-5'>Next.JS Form to Email Example</h1>
            <Form onSubmit={handleSubmit((data) => submitForm(data))}>
                  <Form.Group className="mb-3" controlId="nameField">
                    <Form.Label>Full Name</Form.Label>
                      placeholder="e.g. John Doe"
                    <Form.Control.Feedback type='invalid'>
                  <Form.Group className="mb-3" controlId="emailField">
                    <Form.Label>Email Address</Form.Label>
                      placeholder="e.g. [email protected]"
                    <Form.Control.Feedback type='invalid'>
                <Col lg={12}>
                  <Form.Group className="mb-3" controlId="messageField">
                      placeholder="Please type your message..."
                    <Form.Control.Feedback type='invalid'>
                  <Button variant="primary" type="submit" disabled={isSubmitting}>
                    {isSubmitting ? 'Sending...' : 'Submit'}
            <h1>Thank you!</h1>
            <p>Your message has been received. Please check your email for confirmation.</p>

Run npm run dev and navigate to https://localhost:300 and give 'er a whirl. Hopefully, you'll see this result:

End Result 1

End Result 2

Well, it's been quite a journey! We now have a complete self-contained app that will collect data from a website user and then email it to the admin. And we require no third-party services except Gmail (which is free) and Vercel (which is also free) to host your app.

If you want the full code base for this project, you can clone my repository here.

For more great information, please visit the Designly Blog.