Send Emails From Web Forms in NextJS
Using Twilio SendGrid with NextJS to capture and send form data straight to your email
Intro
Web Forms are a major part of any website and there are lots of ways to handle the data they hold. In this post we are going to explore sending Emails from our web forms within a NextJS application using Twilio SendGrid. You'll need an account with SendGrid Here to get an API key before getting started.
Preview
Let's take a quick look at what we are going to be building for this post. We will be making a simple contact form that will take in a few user inputs and send any valid responses to a provided email. To keep things simple we will add some really minimal styling and try to organize our code in just a few files.
Setting things up
- Create NextJS Application
yarn create next-app sendgrid-example
- Add needed packages
yarn add @sendgrid/mail react-hook-form
- Create necessary pages
touch pages/api/sendEmail.js && touch .env.local && touch pages/contact.js
๐ Verifying Sender Identity ๐
Verify an email address or domain in the Sender Authentication Tab of SendGrid. Without this you will receive a 403 Forbidden response when trying to send mail.
Adding Env Vars
Before we get into writing any code let's open up that .env.local
file and add in our API key from SendGrid. Then we're ready to spin up our API route that will actually handle sending the Email for us.SENDGRID_API_KEY=YOUR-API-KEY-HERE
We will be referencing this env variable in our API route we are creating next.
The Code
Create The API Route to Send an Email
Now that we have things set up we can get to writing some code. We'll start with the API route that will handle communicating with SendGrid. We created the file for this API route already pages/api/sendEmail.js
let's open it up and get to coding. We'll take a peek at the whole API handler function and then break down a few of the pieces.
./pages/api/sendEmail.js
// ./pages/api/sendEmail.js
const sgMail = require('@sendgrid/mail');
export default async function SendEmail(req, res) {
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const { subject, description, email, name } = req.body;
const referer = req.headers.referer;
const content = {
to: ['michael@my.email'],
from: 'michael@my.email',
subject: subject,
text: description,
html: `<div>
<h1>Name: ${name}</h1>
<h1>E-mail: ${email}</h1>
<p>${description}</p>
<p>Sent from: ${referer || 'Not specified or hidden'}`,
};
try {
await sgMail.send(content);
res.status(204).end();
} catch (error) {
console.log('ERROR', error);
res.status(400).send({ message: error });
}
}
Breaking Down the Pieces
Our content variable is where we are going to set the content for our API call to SendGrid. Inside this object we declare the "to" and "from" email addresses as well as the content of the email.
const content = {
to: ['michael@my.email'],
from: 'michael@my.email',
subject: subject,
text: description,
html: `<div>
<h1>Name: ${name}</h1>
<h1>E-mail: ${email}</h1>
<p>${description}</p>
<p>Sent from: ${referer || 'Not specified or hidden'}`,
};
Create The Contact Form
Now we need to create the view for the contact form. For the purpose of this blog post, we will try to keep things simple and in a single file and just do some inline styling. Let's get to it.
./pages/contact.js
// ./pages/contact.js
import {useState} from 'react';
import { useForm } from 'react-hook-form';
import Loading from '../components/Loading';
// Shows when our mail has been successfully sent
const MailSentState = () => (
<div>
<h1>Success! Email has been sent!</h1>
<h1>๐</h1>
</div>
);
// Text displayed when input has error
const ErrorMessage = ({message}) => (
<p
style={{
color: 'red',
fontWeight: 700,
fontSize: '.7rem'
}}
>
{message}
</p>
)
// Row containing input label and error message if any
const LabelRow = ({field, label, message, errors}) => (
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
marginBottom: -15,
}}
>
<p>{label}</p>
{errors[`${field}`] &&
<ErrorMessage message={message}/>
}
</div>
);
// Displays input with label and errors
const InputGroup = ({
register,
field,
label,
message,
errors,
placeholder,
isDisabled,
isRequired,
type
}) => (
<div>
<LabelRow
field={field}
label={label}
message={message}
errors={errors}
/>
{type !== 'textArea'
? (
<input
placeholder={placeholder}
id={field}
disabled={isDisabled}
{...register(field, { required: isRequired })}
/>
)
: (
<textarea
style={{padding: '10px'}}
placeholder={placeholder}
id={field}
disabled={isDisabled}
{...register(field, { required: isRequired })}
/>
)
}
</div>
);
// Our exported Contact component
export default function Contact() {
const [loading, setLoading] = useState(false);
const [hasSuccessfullySentMail, setHasSuccessfullySentMail] = useState(false);
const [hasErrored, setHasErrored] = useState(false);
const { register, handleSubmit, formState } = useForm();
const { isSubmitSuccessful, isSubmitting, isSubmitted, errors } = formState;
// Submit the data to SendGrid to send the email
async function onSubmit(payload) {
setLoading(true);
try {
console.log({ subject: 'Email from contact form', ...payload })
const res = await fetch('/api/sendEmail', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
subject: 'Email from contact form',
...payload
}),
});
// SendGrid only sends 204 on success - if not 204 we set error
if (res.status !== 204) {
setHasErrored(true);
setLoading(false);
}
} catch {
setHasErrored(true);
setLoading(false);
return;
}
setLoading(false)
setHasSuccessfullySentMail(true);
}
const isSent = isSubmitSuccessful && isSubmitted;
const isDisabled = isSubmitting || isSent;
const isSubmitDisabled = Object.keys(errors).length > 0 || isDisabled
if (hasSuccessfullySentMail) {
return (
<MailSentState />
)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
{hasErrored &&
<p>Couldn't send email. Please try again.</p>
}
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '10%'
}}
>
<InputGroup
errors={errors}
label={'Name'}
field={'name'}
message={'Name is required'}
placeholder="Your Name"
register={register}
disabled={isDisabled}
isRequired={true}
/>
<InputGroup
errors={errors}
label={'Email'}
field={'email'}
message={'Email is required'}
placeholder={'Your Email'}
register={register}
disabled={isDisabled}
isRequired={true}
/>
<InputGroup
errors={errors}
label={'Message'}
field={'description'}
message={'Message is required'}
register={register}
disabled={isDisabled}
isRequired={true}
type={'textArea'}
/>
<button
style={{
margin: '10px',
backgroundColor: isSubmitDisabled ? '#ccc' : 'white',
cursor: isSubmitDisabled ? 'not-allowed' : 'pointer',
border: 'none',
display: 'inline-block',
textDecoration: 'none',
textAlign: 'center',
padding: '.75rem 1.25rem',
fontSize: '1.2rem',
color: '#000000',
textTransform: 'uppercase',
fontWeight: 700,
borderRadius: '0.4rem',
border: '2px solid',
cursor: 'pointer',
}}
as="button"
type="submit"
disabled={isSubmitDisabled}
>
{loading ? < Loading/> : 'Send Message'}
</button>
</div>
</form>
)
}
Email Sent!
With that we should have everything we need to send form data responses as an email using NextJS and Twilio SendGrid!
As you can see, it is quite simple to send form data over email using Twilio SendGrid and NextJS! I have used this technique on my personal site micurran.dev in the past with great success.
Next Steps & Final Thoughts
Okay so now we know how to send an email containing form data from our NextJS applications. What's next, how do we make this better?
Additionally we could...
- Redirect the user after successful submission
- Also add user responses to a database
- Send a separate confirmation email to the user with SendGrid
Common Issues
403 Error on form submit
You need to verify an email address or domain in the Sender Authentication Tab of SendGrid. Without this you will receive a 403 Forbidden response when trying to send mail.
Missing API Key
Make sure that you have added your API key from SendGrid to your.env.local
file.
Thanks for reading! ๐