How to send email forms using Next.js 13 with nodemailer

James Owen
6 min readSep 18, 2023

--

This article is to help anyone unsure of how to implement an email form using Next.js 13 which takes user input and sends an email, using nodemailer, to a given email address.

The full source code is available at https://github.com/jamoowen/next.js-email-form-tutorial

Prerequisites: Basic knowledge of Javascript, NPM and Next.js/React

Software: Node.js 16.14 or later

Once you have downloaded or upgraded Node (if necessary), you can proceed.

1. Installation

npx create-next-app@latest
What is your project named? email-test
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What import alias would you like configured? @/*
npm install nodemailer

*These settings aren't necessary for the form, they are simply my preference.

I will create two pages at this point — a home page (/) and and a contact page (/contact).

I deleted all the code in the src/app/page.js file, and replaced it with the minimum amount of code for this tutorial, avoiding styling:

2. Front end


import Link from "next/link"

export default function Home() {

return (
<>

<main className="flex min-h-screen flex-col items-center" >
<div className="relative flex place-items-center p-5 bg-white text-black">
<Link href="/contact">Contact Me</Link>
</div>

</main>
</>

)
}

I will create a contact folder and page to house the form, however you can implement the form within any route in your app.

// My project directory will look like this 
src/app
/contact
page.jsx
page.jsx
layout.jsx

I’ll now add the shell of the contact form within src/app/contact/page.jsx

"use client"
import Link from "next/link"

export default function Contact() {
return (
<main className="flex min-h-screen flex-col items-center" >
<div className="relative flex place-items-center p-5 bg-white text-black">
<Link href="/">Home</Link>
</div>

<form onSubmit={handleSubmit} className="mt-8 mb-2 w-80 max-w-screen-lg sm:w-96">
<div className="mb-4 flex flex-col w-500">

<label htmlFor="form-name">Name </label>
<input id="form-name" autoComplete="name" maxLength={50} size="lg" name="name" className="text-black"/>

<label htmlFor="form-email"> Email:</label>
<input id="form-email" required autoComplete="email" maxLength={80} name="email" type="email" className="text-black"/>

<label htmlFor="form-message"> Message: </label>
<textarea id="form-message" required name="message" rows={5} className="text-black"/>

</div>
<button className=" rounded bg-sky-400" type="submit">Send</button>
</form>
</main>
)
}

if you now run

npm run dev

You can access your app via http://localhost:3000/

We need to create the event handler for when you submit the form:

3. Event handler

async function handleSubmit(event) {
event.preventDefault();

const formData = new FormData(event.target)

const response = await fetch('/api/contact', {
method: 'post',
body: formData,
});
};

This event handler sends a POST request to the /api/email endpoint with the data contained in the formData object. The server at that endpoint should handle the incoming POST request, process the form data, and respond accordingly.

event.preventDefault() prevents the browser from reloading the page

We still need to add a few things to that function, but lets first create the api endpoint and come back to it.

4. Environment variables

Before we create the endpoint, we need to create some environmental variables which will allow nodemailer to log into an email account and send emails on behalf of that.

Next.js 13 has built in support for environmen variables which you can read about here: docs

For local testing we need create a .env.local file here: src/app/.env.local


NEXT_PUBLIC_EMAIL_USERNAME=secret.account@outlook.com
NEXT_PUBLIC_EMAIL_PASSWORD=password123
NEXT_PUBLIC_PERSONAL_EMAIL=myemail@gmail.com

we will now be able to access these variables without revealing them to the client. Next.js usually adds this file to .gitignore but you should double check it is there; the last thing you want is to push your login details to your repo.

  • If you are deploying to production I would not recommend this approach. If you are using Vercel for example, there is environment variable support which you should use instead.

5. Api endpoint

Next.js 13 allows us to create custom request handlers for a given route, if using the app directory (which we are using)

We will create the api route in src/app/api/contact/route.jsx

With Next.js 13 you need to name the exported functions in an api route, after the http methods you will be using. In our example we need to export at least a POST function, since that is the method we are using to submit our form.

import { NextResponse, NextRequest } from 'next/server'
const nodemailer = require('nodemailer');

// Handles POST requests to /api

export async function POST(request) {
const username = process.env.NEXT_PUBLIC_BURNER_USERNAME;
const password = process.env.NEXT_PUBLIC_BURNER_PASSWORD;
const myEmail = process.env.NEXT_PUBLIC_PERSONAL_EMAIL;

const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
const message = formData.get('message')

}

Above, we import NextResponse and nodemailer and retrieve the formdata from the request. The request can handle the FormData object, which is cool, and doesn't need it to be changed to json at all.

We now need to create the nodemailer object which will send the email for us:

const transporter = nodemailer.createTransport({
host: "smtp-mail.outlook.com",
port: 587,
tls: {
ciphers: "SSLv3",
rejectUnauthorized: false,
},

auth: {

user: username,
pass: password
}
});

Ive used outlook for this, but you can use any email provider that uses smtp; you just need to google the correct port and host name.

Next we need to write the code to actually send the email. I wrap this in a try except block to catch any errors.

I pass some parameters to the transporter object ->

from:<the username of the email account you want to use>
to: <where you want the email to land up>
replyTo: <the email address of the sender>
subject: <the subject of the sent email>
html: <html representing the body of the email>

If successful, this will return a response to client who submitted the form.

try {

const mail = await transporter.sendMail({
from: username,
to: myEmail,
replyTo: email,
subject: `Website activity from ${email}`,
html: `
<p>Name: ${name} </p>
<p>Email: ${email} </p>
<p>Message: ${message} </p>
`,
})

return NextResponse.json({ message: "Success: email was sent" })

} catch (error) {
console.log(error)
NextResponse.status(500).json({ message: "COULD NOT SEND MESSAGE" })
}

Now we just need to go back to the page where we created the contact form and tidy up the event handler:


"use client"
import Link from "next/link"

export default function Contact() {

async function handleSubmit(event) {

event.preventDefault();
const formData = new FormData(event.target)
try {

const response = await fetch('/api/contact', {
method: 'post',
body: formData,
});

if (!response.ok) {
console.log("falling over")
throw new Error(`response status: ${response.status}`);
}
const responseData = await response.json();
console.log(responseData['message'])

alert('Message successfully sent');
} catch (err) {
console.error(err);
alert("Error, please try resubmitting the form");
}
};

return (
<main className="flex min-h-screen flex-col items-center" >
<div className="relative flex place-items-center p-5 bg-white text-black">
<Link href="/">Home</Link>
</div>

<form onSubmit={handleSubmit} className="mt-8 mb-2 w-80 max-w-screen-lg sm:w-96">
<div className="mb-4 flex flex-col w-500">

<label htmlFor="form-name">Name </label>
<input id="form-name" autoComplete="name" maxLength={50} size="lg" name="name" className="text-black"/>

<label htmlFor="form-email"> Email:</label>
<input id="form-email" required autoComplete="email" maxLength={80} name="email" type="email" className="text-black"/>

<label htmlFor="form-message"> Message: </label>
<textarea id="form-message" required name="message" rows={5} className="text-black" />

</div>
<button className=" rounded bg-sky-400" type="submit">Send</button>
</form>
</main>
)
}

If you now submit a form, you should see the successful alert popup and the email arrive in your email (there is a good chance it will go to spam initially)

The source code is available on https://github.com/jamoowen/next.js-email-form-tutorial

Please feel free to reach out if you have any questions. I have a variety of contact methods on my website at https://james-owen.vercel.app/contact

Thanks for reading!

--

--