Set up Nodemailer with Next.js & Typescript
Set up Nodemailer with Next.js, Typescript & React Hook Form
Introduction
Nodemailer is a module for Node.js application to allow easy as cake email sending!
Nodemailer allows services like Gmail, SendGrid, Mailgun, SendinBlue, and more. In this article, I will show you how to set up Nodemailer with Next.js/Typescript.
We are going to use React Hook Form but it is optional.
What are some reasons to use Nodemailer?
- To subscribe to a newsletter
- Transactional emails
- Sending automated messages
- User verification emails
- Receive messages from a contact form.
Prerequisites
- Basic knowledge of Next.js
- Basic knowledge of Typescript
- Basic knowledge of React Hook Form
We will begin by starting a fresh new Next.js app with typescript and sign up for SendinBlue. SendinBlue provides 300 free emails daily, but feel free to use any SMTP service of your choice!
When creating a SendinBlue account please find your SMTP KEY PASS. This could differ if you are using a different service. Please refer to the nodemailer documentation.
NOTE: Not sure where to find your SMTP pass after signing up? Click on your name on the top right > SMTP & API, and click on the SMTP tab near the center.
Start your Next.js app with the following command:
npm run dev
or with yarn or pnpm π
Let's get started!
In your .env.local
, add the following:
SMTP_PASS='YOUR_SMTP_PASS'
Create a file named contact.ts
under pages/api/
and add this small boilerplate below:
// pages/api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import * as nodemailer from 'nodemailer';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// code...
}
Let's add the createTransport
method provided by Nodemailer. This allows us to set up our SMTP service.
const transporter = nodemailer.createTransport({
service: 'SendinBlue',
auth: {
user: 'johndoe@gmail.com', // The account you signed up with SendinBlue
pass: process.env.SMTP_PASS,
},
secure: false,
});
You can see the other possible options to pass in here.
So, what type of mail data do we need? Nodemailer offers a lot of options, but we will only need the following for this article:
- From
- To
- Subject
- Text
- Html
Other options available here
And for demonstration purposes, I will send an email to myself.
const { name, email, message } = req.body;
const mailData = {
from: email,
to: email,
subject: `Message from ${name}`,
text: `${message} | Sent from: ${email}`,
html: `<div>${message}</div><p>Sent from: ${email}</p>`,
};
As you can see, we are grabbing our data from the req.body
One of the cons (not a big deal) is they donβt provide async/await off the bat so letβs create our own promise.
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// rest of code ...
// ....
await new Promise((resolve, reject) => {
transporter.sendMail(mailData, (err: Error | null, info) => {
if (err) {
reject(err);
return res
.status(500)
.json({ error: err.message || 'Something went wrong' });
} else {
resolve(info.accepted);
res.status(200).json({ message: 'Message sent!' });
}
});
});
return;
}
To receive the information from SendinBlue dashboard, we use the sendMail
method from Nodemailer. As you can see, we are passing our mail data options from earlier. This will let us to know if something went wrong or if everything went through fine.
One thing I left out is handling errors. Here is something we can add to our code above the mailData
object
const { name, email, message } = req.body;
if (!message || !name || !message) {
return res
.status(400)
.json({ message: 'Please fill out the necessary fields' });
}
You should be able to test the endpoint at http://localhost:3000/api/contact
in Postman, Insomnia, or any other tools to check if everything is working fine!
Feel free to exclude information on purpose to ensure errors are working.
Here is the final code for the contact.ts
file if it is not working for you!
// pages/api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import * as nodemailer from 'nodemailer';
// Nodemailer docs: // https://nodemailer.com/about/
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// https://nodemailer.com/smtp/
const transporter = nodemailer.createTransport({
service: 'SendinBlue',
auth: {
user: process.env.email,
pass: process.env.SMTP_PASS,
},
secure: false, // Default value but showing for explicitness
});
const { name, email, message } = req.body;
if (!message || !name || !message) {
return res
.status(400)
.json({ message: 'Please fill out the necessary fields' });
}
// https://nodemailer.com/message/#common-fields
const mailData = {
from: email,
to: email,
subject: `Message from ${name}`,
text: `${message} | Sent from: ${email}`,
html: `<div>${message}</div><p>Sent from: ${email}</p>`,
};
await new Promise((resolve, reject) => {
transporter.sendMail(mailData, (err: Error | null, info) => {
if (err) {
reject(err);
return res
.status(500)
.json({ error: err.message || 'Something went wrong' });
} else {
resolve(info.accepted);
res.status(200).json({ message: 'Message sent!' });
}
});
});
return;
}
Let's move over to the client side!
We will not dive deep into UI or CSS much. Please look at the Github link below to see the basic styling I have provided.
Here is a simple boilerplate to begin with to render our form:
// pages/index.tsx
import { useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useForm } from 'react-hook-form';
const Home: NextPage = () => {
return <div>Nodemailer!</div>;
};
export default Home;
Since we are working with TypeScript let's add our interface to match our form data above our Home
function
interface DataProps {
name: string;
email: string;
message: string;
}
As a reminder, we are using React Hook Form. Feel free to use other libraries of your liking!
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: {
name: '',
email: '',
message: '',
},
});
- The register is to provide us with many options.
- The handleSubmit function allows us to submit our form data.
- The formState object will allow us to check if there are any errors in our form.
Let's add the following function to handle our form submission:
const onSubmit = async (data: DataProps) => {
console.log(data);
};
and then we can set up our form as such:
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input id="name" {...register('name', { required: true })} type="text" />
<label htmlFor="email">Email</label>
<input id="email" {...register('email', { required: true })} type="email" />
<label htmlFor="message">Message</label>
<input id="message" {...register('message', { required: true })} type="text"/>
<button type="submit">Submit</button>
</form>
</div>
</div>
)
NOTE: Styling and other important factors are intentionally excluded above.
On the client side, you should now be able to submit the data. Check the console log to make sure everything is going through!
Cool, but letβs get our data sent to SendinBlue with the email provided.
Letβs add the following information to the onSubmit function we created earlier:
const onSubmit = async (data: DataProps) => {
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const body = await res.json();
if (res.ok) {
alert(`${body.message} π`);
}
if (res.status === 400) {
alert(`${body.message} π’`);
}
} catch (err) {
console.log('Something went wrong: ', err);
}
};
In this function, we are targeting the contact endpoint we created earlier. We are also passing in the data we collected from our form.
Please give it a go! You should now be able to send an email.
WOOHOO ππ½π To see the information on SendinBlue dashboard, click the Transactional tab.
If you made it this far, you should now have a fully functional Nodemailer service!
You can find the full code below, which includes items that were abstracted from you (including the errors!):
import { useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useForm } from 'react-hook-form';
interface DataProps {
name: string;
email: string;
message: string;
}
const Home: NextPage = () => {
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: {
name: '',
email: '',
message: '',
},
});
const onSubmit = async (data: DataProps) => {
try {
setIsLoading(true);
const res = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const body = await res.json();
if (res.ok) {
alert(`${body.message} π`);
}
if (res.status === 400) {
alert(`${body.message} π’`);
}
setIsLoading(false);
} catch (err) {
console.log('Something went wrong: ', err);
}
};
return (
<div>
<Head>
<title>Nodemailer with Next.js</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.container}>
<h3 className={styles.text}>Contact me!</h3>
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name" className={styles.label}>
Name
</label>
<input
id="name"
className={styles.input}
{...register('name', { required: true })}
type="text"
/>
{errors.name && <p className={styles.error}>{errors.name.type}</p>}
<label htmlFor="email" className={styles.label}>
Email
</label>
<input
id="email"
className={styles.input}
{...register('email', { required: true })}
type="email"
/>
{errors.email && <p className={styles.error}>{errors.email.type}</p>}
<label htmlFor="message" className={styles.label}>
Message
</label>
<input
id="message"
className={styles.input}
{...register('message', { required: true })}
type="text"
/>
{errors.message && (
<p className={styles.error}>{errors.message.type}</p>
)}
<button type="submit" disabled={isLoading} className={styles.button}>
{isLoading ? 'loading...' : 'submit'}
</button>
</form>
</div>
</div>
);
};
export default Home;
Full code here: Link