Coding a Contact Form with Next.js and Nodemailer

Elyssa Winch
Nerd For Tech
Published in
13 min readMar 24, 2021

--

Photo by Volodymyr Hryshchenko on Unsplash

Hey! Want to snazz up your portfolio with an email contact form? Or give clients a way to contact you directly without having to leave your site? Lucky for you, setting up a simple email contact form with Next.js and Nodemailer is a snap. Let me walk you through it.

For clarity: This is a tutorial for how to code a custom email contact form. At the end, you’ll be able to embed a form on your site that takes in the user’s name, email, and message (And any other information you might want.) and sends an email containing that info to a Gmail address. That can be your Gmail, a Gmail the user puts in, you could send the same message to multiple addresses or send tailored messages to several accounts with one action.

I’ll assume that you have basic knowledge of Javascript, Node, and Next.js. You could make this work with plain React as well, or any other front-end framework, but I’m using Next since it handles the server code for us.

If you want to see a working example of one such form, check out the Contact form at the bottom of my portfolio here!

Step One: Create Next App

Open up your terminal to the folder of your choice, and create your Next.js app. You know how this goes - Just type into terminal:

npx create-next-app

You should end up with a typical starter Next app

Step Two: Add the Form

Once you’ve got the app made, clean out the Next boilerplate code so that you have a nice and clean Next app.

git on outta here, boilerplate

Now, add a Form, which should contain an input field for the user’s Name, Email, and Message. And a Submit button, of course.

Remember to use htmlFor when working with labels in React and Next!
<div className={styles.container}>
< form className={styles.main} >
< formGroup className={styles.inputGroup} >
< label htmlFor='name'>Name</label>
< input type='text' name='name' className={styles.inputField} />
</formGroup>
< formGroup className={styles.inputGroup} >
< label htmlFor='email'>Email</label>
< input type='email' name='email' className={styles.inputField} />
</formGroup>
< formGroup className={styles.inputGroup} >
< label htmlFor='message'>Message</label>
< input type='text' name='message' className={styles.inputField} />
</formGroup>
< input type='submit'/>
</form >
</div>

I added a little bit of styling to my form as well, just to make it a little easier to look at as we go along.

.inputGroup {
height: 50%;
width: 200%;
display: flex;
flex-direction: column;
margin: 10px 0;
}
.inputLabel {
text-align: left;
}
.inputField {
height: 30px;
}

At the end of it, you should have a form that looks like this (Well, assuming you copied my styling.)

Cool! Now let’s move on.

Step Three: Set up front-end functionality

Let’s get our front-end working before we try to plug it into anything fancy. We’ll need to import useState from React in order to hold input that the user gives us, so let’s start with that first.

Outside of your export function, write:

import { useState } from 'react'

And inside of your export, but outside the return, write:

const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [message, setMessage] = useState('')
const [submitted, setSubmitted] = useState(false)

In order: These are state variables to hold the user’s name, email, and message, and a Boolean value to flag if the message has been submitted or not. Now we need to pass those values from the input fields to the state variables. Let’s start with the onChange function.

‘onChange’ is a attribute that we set inside of HTML tags, such as input fields, that will run a function whenever the value of that tag changes — such as, for instance, when the user types their name into an empty box. Inside of this onChange function, we can use our state variable setter functions to pass the value of the changed input field to state, thereby capturing the user input.

So, let’s add our onChange functions to Name, Email, and Message. As an example, our Name input should go from:

< input type='text' name='name' className={styles.inputField} />

to:

< input type='text' onChange={(e)=>{setName(e.target.value)}} name='name' className={styles.inputField} />

The ‘onChange={(e)=>{setName(e.target.value)}’ is the important line here. It tells Next that, when this input field registers a change, it should run this anonymous function. Our ‘e’ variable holds information about our event, which we need to pass to the setter in order to capture the input field’s value. The e.target.value takes that value, and sets it as the new value of our name variable in state.

Now do the same for the Email and Message fields.

Remember to change ‘setName’ to ‘setEmail’ and ‘setMessage,’ respectively

Cool! Now we can test this by using our dev tools to check on our State values in the browser. Open up your app, type something into the input fields, and see if your state variables reflect the input you gave.

Bee-ay-utiful. So we’re successfully taking in user input. Now what do we do with it?

Step Four: Handling Submit

When the user hits the submit button, we need to take the information they’ve given us and DO something with it. A form that takes data and then dumps it out with no action is a pretty useless form, right?

So now we need to bring in Fetch in order to get this data going somewhere. (You can use Axios if you’d like, or any other alternative, but for the sake of simplicity, I’m sticking with Fetch.) All we need is, when the user submits, to take their data and post it to an API.

In your functional component but before your return, create a new function:

const handleSubmit = (e) => {
[ Code goes here, eventually ]
}

We don’t need to pass anything to this function besides the event — everything it needs, it will pull from State. Inside of it, create an object called data, to store our state variables. Oh, and a e.preventDefault(), so that the page doesn’t just reload every time.

const handleSubmit = (e) => { 
e.preventDefault()
console.log('Sending')
let data = {
name,
email,
message
}
})

Now, let’s add the Fetch request:

const handleSubmit = (e) => { 
e.preventDefault()
console.log('Sending')
let data = {
name,
email,
message
}
fetch('/api/contact', {
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
})

What’s all this doing? It’s a Fetch call to the url ‘/api/contact,’ which we’ll get to later. We need it to post to that url, and we need to send along JSON of our data object, which contains user information such as name and email.

Lastly, we’ll want to handle what should happen when that call is returned, so that we can show the user that their action went through:

const handleSubmit = (e) => { 
e.preventDefault()
console.log('Sending')
let data = {
name,
email,
message
}
fetch('/api/contact', {
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then((res) => {
console.log('Response received')
if (res.status === 200) {
console.log('Response succeeded!')
setSubmitted(true)
setName('')
setEmail('')
setBody('')
}
})
})

With a .then chained on the end of the Fetch call, we reset our values back to null and set submitted to true.

Now, lastly, let’s wrap our handleSubmit function in an onClick in the submit button.

< input type='submit' onClick={(e)=>{handleSubmit(e)}}/>

This will run the handleSubmit function whenever we click submit, so the user input will be collected and posted by the Fetch call to our /api/contact url.

Let’s test it out by opening our console, inputting some information, and hitting submit. We should get a ‘Sending’ log, but not much else, since the Fetch is currently posting to a non-existent URL.

We got a ‘Sending’ — that’s good! — and a 404 error. Okay! That’s what we want, actually. Check the 404 message — is it posting to the /api/contact url? If so, that’s exactly what we want to see.

Now, that’s the front-end. How about the back? How do we clear that 404 error and starting sending emails?

Step Five: Build the API Route

Let’s use that sweet, easy API functionality of Next to build out an endpoint for our Fetch. In the API folder, create a new file named contact.js

Now, inside of contact.js, create a new export default function, with (req, res) as the parameters, and have it console log req.body:

export default function (req, res) {
console.log(req.body)
}

Now, let’s test that our app components are communicating as they should. Boot up your app, go back to your form, and submit your information. You should get no 404 errors in your console now. What you SHOULD get, however, is your submitted name/email/message showing up in your terminal log:

Your form submission is now successfully posting to your API route! Now we can do whatever we like with this input. No worries about giving the user Too Much Information on sensitive subjects like our email password. Onto the next step!

Step Six: But What About Nodemailer

For a tutorial on Nodemailer, we’ve used absolutely no Nodemailer thus far. That changes now! Safely ensconced in our server, we can start pulling in Nodemailer and actually sending some emails. To begin, download Nodemailer. In Terminal, write:

npm i nodemailer

Now, in your contact.js api route, import Nodemailer, and create an object called ‘transporter’ with nodemailer.createTransport()

export default function (req, res) {  let nodemailer = require('nodemailer')
const transporter = nodemailer.createTransport({});
console.log(req.body)
}

The transporter object is, essentially, the object that stores all the information on how we want to send our emails — what account are we using? What provider? What port? We’ll define all of these on the transporter object, like so:

  const transporter = nodemailer.createTransport({
port: 465,
host: "smtp.gmail.com",
auth: {
user: 'demo@demo.gmail',
pass: 'password',
},
secure: true,
})

Here are the major features of this transporter: port, host, and auth. Port refers to the communication ‘channel’ that the email will use — 465 is for SMTP communication, which we designate in the ‘host’ line beneath it. None of this is super crucial to what we’re doing here, but if you’re interested in knowing more, research into port 465 and SMTP.

What we do care about, however, is auth. Notice it contains properties for ‘user’ and ‘pass’ — this is the username and password of the account that we will be sending our emails from.

Yes — For obvious security reasons, the user can’t send us an email from their direct account. What we can do, however, is take their message and email it to ourselves via a dummy account that we created. You could just pass in your personal Gmail and password, and send yourself these messages directly, but to safeguard your personal account, I would recommend creating a burner whose sole purpose is sending these emails. That way, if the account is compromised, your personal information remains protected.

So, let’s take a pause from coding and move onto the next step: Setting up the dummy account

Step Seven: Creating the Account

Create your dummy Gmail account the same way you’d create any Gmail account — by going to Gmail and simply creating it. There’s no real difference between your bot account and a proper Gmail, with one exception.

Once your account is created, you’ll need to adjust its security settings to allow programmatic access. Otherwise, even if your code runs perfectly, Gmail will still block the message under the belief that it’s a security breach. We have to tell Gmail to allow less-secure access to the account, which is another reason why using a burner instead of your personal is a good idea.

On the account’s page, click on its icon in the upper right, and click on Manage your Google Account:

Now, go to the Security tab on the left, scroll down until you see a section titled ‘Less Secure App Access.’ Set it to Allow.

Now, we’ve got our account set up to allow programmatic access. But isn’t there one more security step we should be taking? Consider our burner account’s password: It’s a burner, sure, but do we really want to be sticking its password into our code? In plain text?

To give our account a little more security, we need to set up our API route to pull in the password as an environment variable. So, let’s call in our old friend, dotenv.

In Terminal, inside your project folder, type:

npm i dotenv

Then, create your .env file with

touch .env

Next, in your .env file, write:

password=Whatever your account password is

And lastly, at the top of your contact.js API route, import your .env with:

require('dotenv').config()

And pull your account password in from the .env

const PASSWORD = process.env.password

And we’re golden! If you’re sharing this code to github, remember to include the .env in your gitignore. And if you’re deploying the app to a live site, remember to include your password as an environment variable!

Okay. NOW let’s start sending some emails.

Step Eight: Putting It All Together

So, we’ve got our burner account all set up, our form taking in input, and our API spitting that input back out on the server-side. Last thing to do is to get it all working, right?

Going back to the contact.js code, there are two final things we have to do: Create the email that we’d like to send, and then we have to send it! Let’s start with the first. Beneath your transporter object, write:

  const mailData = {
from: 'demo@demo.com',
to: 'your email',
subject: `Message From ${req.body.name}`,
text: req.body.message,
html: <div>{req.body.message}</div>
}

We create a new object, called ‘mailData,’ which contains five fields: a From, a To, a Subject, Text, and HTML. Seem familiar? These are the input fields of an email —

From is the address sending it (Our burner account).

To is the recipient (You can designate multiple, if you’d like to send it to several people.)

Subject is the subject that the email will send with.

And Text is the plain-text body of the message, with HTML allowing for formatting with <div> tags and the like.

Thanks to the API route, we can take the information that the user input on the Contact form page and inject it into the email that we would like to send. Simply use req.body.name, req.body.email, and req.body.message for each respective variable (And naturally, if you have any custom variables, use the same req.body.[variable name] format.)

We can also style the content however we like under the HTML property, in order to make it more readable. My recommendation is to include the user’s message, name, and email, so that you can follow up on the message at your leisure. Imagine if you sent the user’s message along with no email attached — you’d never be able to contact them again.

Now, we’ve configured the sender and the email it needs to send. The last major piece is actually sending that email! Luckily, this is just a simple function call. After your mailData object, write:

transporter.sendMail(mailData, function (err, info) {
if(err)
console.log(err)
else
console.log(info)
})

This function will send the email along, and if any sort of error comes up, it’ll output that error to the console for troubleshooting.

And lastly, since this is all made possible through an API call, we need to end the route with a .send, so that our front-end knows if the call succeeded or not. Just before the function close, write:

res.status(200)

This is the final piece of code. In totality, here’s what your entire contact.js should look like:

export default function (req, res) {
require('dotenv').config()

let nodemailer = require('nodemailer')
const transporter = nodemailer.createTransport({
port: 465,
host: "smtp.gmail.com",
auth: {
user: 'demo email',
pass: process.env.password,
},
secure: true,
})
const mailData = {
from: 'demo email',
to: 'your email',
subject: `Message From ${req.body.name}`,
text: req.body.message + " | Sent from: " + req.body.email,
html: `<div>${req.body.message}</div><p>Sent from:
${req.body.email}</p>`
}
transporter.sendMail(mailData, function (err, info) {
if(err)
console.log(err)
else
console.log(info)
})
res.status(200)
}

And there we go! If it’s all connected through correctly, you should be able to automatically send an email containing user input from the contact form. Try to test it all yourself by writing your own email into the Contact form page, submitting, and waiting to see if you get the submission in your email inbox.

For the front-end side, you can expand on this as well — As a start, you could set the user input fields back to empty and display a message on a successful response, just to add some extra communication to the user that it all went through. You could format the HTML in your email, to make it generally more aesthetically pleasing. Or you could just celebrate the fact that your contact form gets the user to stay on your site a little longer! Maybe they’ll buy something in the point-five seconds it takes to hit submit, who knows.

If you got this far and got it working, congratulations! If you’d like to take a peek at my code in full, you can find it on Github here. If you liked what I wrote, message me on Linkedin or hit me up on my own swanky Contact form at my portfolio!

Thanks for reading, and happy coding!

Github code

--

--