In this guide, I will walk through the process of sending emails from a Next.js application hosted on Vercel. To handle the email-sending functionality, I’ll use Nodemailer, a popular Node.js module for sending emails. This approach includes:
- Setting up a Next.js project
- Installing and configuring Nodemailer
- Sending emails in Vercel
- Testing email functionality
Disclaimer: For the code from this article to work, you’ll need Next.js 13.4 or 14.x, Nodemailer 6.9.x or later, and Node.js 18.x. or 20.x.
Create a Next.js project
By the end of this guide, you’ll have a fully functional email-sending setup deployed on Vercel. To implement it, we’ll use Vercel Functions. You can read about them in more detail here or on the official documentation page.
Prerequisites:
- A Vercel account – You can register by visiting this link and the process is straightforward.
- An email provider for SMTP configuration – In this tutorial, we’ll use Mailtrap SMTP.
- A GitHub account for Vercel deployment workflow.
Okay, let’s get down to some coding.
First, start by creating a Next.js project via npx:
npx create-next-app@latest my-next-email-app
Note: You will be given a few options during the process: if you want to use TypeScript or not, if you want to use ESLint, etc. Feel free to choose the option that matches your preferences. For this example, we’ll use all of them except for the customization of the import alias.
And just like that, we have installed all of our required dependencies and set up a Next.js project!
To navigate to the newly created directory, simply run the following cd (chdir) command, like so:
cd my-next-email-app
Tip: If you’re using a code editor like VSC, I suggest reloading it.
With that out of the way, here’s what your project directory should look like:
Set up Nodemailer
Nodemailer is a popular module that allows you to send emails from Node.js applications. Additionally, you can use it to easily implement email-sending logic in the Vercel Functions.
Fortunately, this time, we won’t need to set up a separate Node.js project since Next.js already provides us with access to a server environment in certain files, where we’ll store the Vercel Function for sending emails.
To install Nodemailer, you can either use the npm package manager:
npm install nodemailer
Or yarn, whichever you prefer:
yarn add nodemailer
Now, let’s set up the SMTP transporter, which is virtually a “box” with all methods preconfigured to work with the SMTP credentials that we pass to it.
For this, create a folder inside src/app called api. Then, inside the api folder, create yet another folder named, for example, send-email. Finally, within send-email, create a route.js file, where you’ll copy the following code snippet:
// app/api/send-email/route.js
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export async function POST(req) {
const { subject, text, to, html } = await req.json();
const mailOptions = {
from: "sender@your-domain",
to,
subject,
text,
html,
};
try {
await transporter.sendMail(mailOptions);
return new Response(
JSON.stringify({ message: "Email sent successfully!" }),
{ status: 200 }
);
} catch (error) {
return new Response(
JSON.stringify({ message: "Error sending email", error }),
{ status: 500 }
);
}
}
Code breakdown:
- We have defined API routes inside the app/api directory.
- We’ve used Nodemailer’s transporter to configure an instance containing email-sending methods with our SMTP credentials.
- The POST function will handle incoming email requests. The function you’re working with should be named after the HTTP method you’re using (in this case, we’re using POST method).
- You can read about Vercel Functions here. 👀
req.json()
method allows us to extract parameters from the body of an HTTP request.- Using
transporter.sendMail
, we attempt to send an email with the appropriate options created from the extracted request body’s parameters. - The global
Response
class will return a successful message or an error from the API route depending on the result of the email sending.
Creating environment variables (recommended)
Now, let’s set up environment variables to store our Mailtrap SMTP credentials safely.
To do this, create an .env.local file in the root folder and add environment variables from your Mailtrap dashboard. You can find the credentials in Sending Domains → Your Domain → Integration → Transactional Stream (Integrate Button).
And here’s how your .env.local file should look like:
SMTP_HOST=your-smtp-host
SMTP_PORT=your-smtp-port
SMTP_USER=your-smtp-user
SMTP_PASS=your-smtp-password
How to send emails in Vercel
To send a plain-text email from your frontend (Next.js client-side), you can create a component where a user can click a button and trigger the API call to send an example email. When we deploy our Next.js application to Vercel, the clicking the button will trigger a corresponding Vercel Function to send the email.
So, navigate to src/app and select the page.tsx file, which we’ll update to with our email-sending logic, where we call the previously created API route.
By default, it should contain some pre-generated code. Remove all of it and insert the following script inside:
"use client";
export default function EmailForm() {
const sendEmail = async () => {
const mailOptions = {
subject: "Test email from Next.js app on Vercel using Mailtrap",
to: "recipient@example.com",
text: "This is a test email sent from a Next.js app deployed on Vercel using Mailtrap."
};
const res = await fetch("/api/send-email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(mailOptions),
});
if (res.ok) {
alert("Email sent successfully");
} else {
alert("Error sending email");
}
};
return (
<div
style={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<button
onClick={sendEmail}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "16px",
}}
>
Send Example Email
</button>
</div>
);
}
Technical details:
- We use the
“use client”
statement now since we have event listeners likeonClick
in the code; this is required to make components accessible in the browser environment in Next.js so that we can interact with those browser events. - We declare the
sendEmail
function containing email-sending logic. First, we create themailOptions
object containing the properties that we want to pass in the request body that we would later retrieve in our API route. - Then, we make a POST request to the API route we previously created, passing
mailOptions
in the request body. After that, we display the email-sending result. - Lastly, we’ll use the
onClick
event listener, which ensures that when the button is clicked, the email-sending code will be called.
Now, to run the application locally and verify that the email-sending functionality works correctly, you can again use either npm:
npm run dev
Or yarn:
yarn dev
Then, open the localhost link and click on the ‘Send Example Email’ button. You should see the following:
If you use Mailtrap’s SMTP, the email will appear in your Email Logs.
Note: Every bit of the code in this article has been tested before publication, so the email-sending logic should work fine. In case it doesn’t, simply double-check your credentials. 🙂
Deploying the Next.js app to Vercel
Right now, our API route behaves just as an API route when we run it locally, not like a Vercel Function as we expect it to. For Vercel to define our API route as a Vercel Function, let’s deploy the Next.js application.
Now, let’s deploy our app:
1. Push your app to GitHub
First, let’s push our Next.js application to a Git Hosting solution of your choice. Vercel allows you to choose from GitHub, GitLab, or Bitbucket. For our example, we’ll use GitHub.
Start by initializing a Git repository for your project by running the following command:
git init
Add all the project files to Git:
git add .
Commit the files with a descriptive message:
git commit -m "Initial commit"
2. Create a new GitHub repository
Then, go to GitHub, create a new repository, and select your preferred options.
Important: I should note that you shouldn’t worry about your SMTP credentials being leaked even if you’re using a Public Repository since in .gitignore Next.js includes all .env files by default, so the credentials from our .env.local file won’t be pushed to GitHub.
Next, create a link to your GitHub repository by running:
git remote add origin https://github.com/your-username/your-repository-name.git
Then, push your code to the GitHub repository with the following command:
git push -u origin main
3. Deploy the Next.js app
Go to the Vercel New App page and select Continue with GitHub.
On the next page, click Adjust Github App Permissions.
Then, choose your account, login, select the new repository and click Save.
The added repository should then appear in the Vercel tab again. After that, click Import.
In the opened window, leave everything the same—just make sure to add your own environment variables, as you can see in the picture, and click Deploy.
If everything was set up correctly, your Next.js app should have been successfully deployed.
If you open the page by link, click the button a few times, and then go to the Observability tab in your project dashboard in Vercel, you’ll notice that now your /api/send-email API Route that we previously created is now treated like a Vercel Function. Again, this allows us to run server-side SMTP solutions like nodemailer in Next.js without setting up our own server.
Also, Vercel Functions dynamically scale based on user demand, efficiently manage API and database connections and leverage fluid compute for improved concurrency, making them ideal for heavy tasks that require seamless scalability.
You can read about it in more detail in the official documentation.
Send HTML email
If you want to send an HTML email, all you need to do is add the html
property and the desired message tucked within tags. It’s as simple as that, check it out:
"use client";
export default function EmailForm() {
const sendEmail = async () => {
const mailOptions = {
subject: "Test email from Next.js app on Vercel using Mailtrap",
to: "recipient@example.com",
text: "This is a test email sent from a Next.js app deployed on Vercel using Mailtrap.",
html: "<h1>HTML Content for test email from Next.js app on Vercel using Mailtrap</h1>",
};
const res = await fetch("/api/send-email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(mailOptions),
});
if (res.ok) {
alert("Email sent successfully");
} else {
alert("Error sending email");
}
};
return (
<div
style={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<button
onClick={sendEmail}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "16px",
}}
>
Send Example Email
</button>
</div>
);
}
Send email with attachment
Since we deployed our app on Vercel and called the Vercel Function to send an email for the first time, let’s make some changes so that our code also includes sending emails with attachments.
The code we’re about to write is going to be quite different. Because of the way files are sent over the network, we’ll use multipart/form-data instead of the json format that we’re currently using to send properties to our API route.
First, let’s update the page.tsx file to include the new approach using FormData:
"use client";
import { useState } from "react";
export default function EmailForm() {
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
const fileInput = e.target.files;
if (fileInput && fileInput.length > 0) {
setFile(fileInput[0]);
}
};
const sendEmail = async () => {
const formData = new FormData();
formData.append(
"subject",
"Test email from Next.js app on Vercel using Mailtrap"
);
formData.append(
"text",
"This is a test email sent from a Next.js app deployed on Vercel using Mailtrap."
);
formData.append("to", "cc57d3ca6e-292fc7@inbox.mailtrap.io");
if (file) {
formData.append("file", file);
}
const res = await fetch("/api/send-email", {
method: "POST",
body: formData,
});
if (res.ok) {
alert("Email sent successfully");
} else {
alert("Error sending email");
}
};
return (
<div
style={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
backgroundColor: "white",
}}
>
<button
onClick={sendEmail}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "16px",
}}
>
Send Example Email
</button>
<input
type="file"
id="file"
name="attachment"
onChange={handleFileChange}
style={{
border: "1px solid #ccc",
padding: "10px",
borderRadius: "4px",
cursor: "pointer",
}}
/>
{file && (
<div style={{ fontSize: "14px", color: "#666" }}>
Selected file: {file.name}
</div>
)}
</div>
</div>
);
}
Key changes:
- We’re using a file input to let the user choose a file. So, when the file is selected, it’s stored in the component’s state (file).
- Now, we use FormData to send the email details (including the file) as a multipart/form-data request.
- On form submission, we’re sending the form data (with the file attachment) to the send-email API route.
- Additionally, we’re showing the selected file to the user for more clarity.
Next, we need to update the code in our route.js file. Since we’re no longer passing the data in requests using json, but passing the data in multipart/form-data format, the implementation is going to be different. Check it out:
// app/api/send-email/route.js
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export async function POST(req) {
try {
const formData = await req.formData();
const subject = formData.get("subject");
const text = formData.get("text");
const to = formData.get("to");
const file = formData.get("file");
const mailOptions = {
from: "sender@freelance.mailtrap.link",
to,
subject,
text,
};
if (file) {
const buffer = await file.arrayBuffer();
mailOptions.attachments = [
{
filename: file.name,
content: Buffer.from(buffer),
contentType: file.type,
},
];
}
await transporter.sendMail(mailOptions);
return new Response(
JSON.stringify({ message: "Email sent successfully!" }),
{ status: 200 }
);
} catch (error) {
return new Response(
JSON.stringify({ message: "Error sending email", error }),
{ status: 500 }
);
}
}
What’s changed:
- The
req.formData()
method extracts thefromData
object from the request. - We extract properties from the form one by one using the
fromData.get
method, including the file property. - We read the file contents using the
file.arrayBuffer()
method, and pass it to Nodemailer options together with filename and file type. Or, if the file isn’t attached, we skip it.
Before you push and deploy your new code, make sure to test that you don’t have any errors when running locally.
Again, you can use npm:
npm run dev
Or yarn:
yarn dev
Once you’ve verified that everything works correctly, let’s push the new code to GitHub by running the following commands:
git add .
git commit -m "Add file attachment"
git push -u origin main
What’s nice about using Vercel is that we don’t have to focus on updating anything manually. As soon as the new code is pushed to GitHub, Vercel will redeploy the app with new changes for us. Open the live version of your website in the Vercel dashboard.
As you open the updated Vercel Application, you will see the new file attachment feature we implemented. This way, we can attach new files easily, and they’ll be included in Email Attachments.
And here’s how it looks in the Mailtrap Email Logs:
Send email to multiple recipients
To send emails to multiple recipients, all you need to do is pass them as a comma-separated string on the page.tsx file and let Nodemailer handle it.
Feel free to use the following script:
"use client";
import { useState } from "react";
export default function EmailForm() {
const [file, setFile] = useState<File | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const fileInput = e.target.files;
if (fileInput && fileInput.length > 0) {
setFile(fileInput[0]);
}
};
const sendEmail = async () => {
const formData = new FormData();
const recipients = ["recipient1@example.com", "recipient2@example.com"];
formData.append(
"subject",
"Test email from Next.js app on Vercel using Mailtrap"
);
formData.append(
"text",
"This is a test email sent from a Next.js app deployed on Vercel using Mailtrap."
);
formData.append("to", recipients.join(", "));
if (file) {
formData.append("file", file);
}
const res = await fetch("/api/send-email", {
method: "POST",
body: formData,
});
if (res.ok) {
alert("Email sent successfully");
} else {
alert("Error sending email");
}
};
return (
<div
style={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 8,
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
backgroundColor: "white",
}}
>
<button
onClick={sendEmail}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "16px",
}}
>
Send Example Email
</button>
<input
type="file"
id="file"
name="attachment"
onChange={handleFileChange}
style={{
border: "1px solid #ccc",
padding: "10px",
borderRadius: "4px",
cursor: "pointer",
}}
/>
{file && (
<div style={{ fontSize: "14px", color: "#666" }}>
Selected file: {file.name}
</div>
)}
</div>
</div>
);
}
Key changes:
- Used the recipients array to have a list of mock recipients to whom we want to send our email.
- Passed the array as a string with all recipients delimited by a comma, using the recipients.join(“, “) method. It transforms the array into a string this way
["recipient1@example.com", "recipient2@example.com"]
→
“recipient1@example.com, recipient2@example.com”
As always, before you push and deploy your new code, make sure to test that you don’t have any errors when running locally.
npm:
npm run dev
yarn:
yarn dev
All works as it should? Let’s push new code to GitHub by running:
git add .
git commit -m "Add multiple recipients"
git push -u origin main
Vercel will then redeploy the app with new changes for us.
Now, you can open the live version of your website in the Vercel dashboard and click the button to send emails to the mentioned recipients. As you open your Email Logs tab in the Mailtrap dashboard, you should see 2 new emails successfully sent to these recipients accordingly:
Test email and email sending
When building applications that involve email functionality, it’s essential to test email workflows in a safe and controlled environment before launching to production. You wouldn’t want to wear a brand-new suit to a wedding without trying it on first, would you? 🤵
Similarly, sending emails from a staging environment helps ensure:
- Your emails or templates are correctly formatted and rendered well across different devices and email clients.
- You don’t accidentally deliver your emails to real users.
- Your domain won’t get blacklisted for spam.
- You have a clear picture of important metrics like deliverability, spam scores, and more.
For this, I personally use Mailtrap Email Testing, another essential part of the Mailtrap Email Delivery Platform, which allows me to do all of the above.
To start testing emails in your Next.js app we’ve made in this article, all you need to do is:
- Create a free Mailtrap account.
- Navigate to Email Testing and choose your inbox.
- Retrieve your SMTP credentials for the testing inbox.
- Insert the fake SMTP credentials in the .env.local file, just like so:
SMTP_HOST=sandbox.smtp.mailtrap.io
SMTP_PORT=25
SMTP_USER=your-testing-username
SMTP_PASS=your-testing-password
- Run the code with, you’ve guessed it, either npm or yarn commands:
##npm
npm run dev
##yarn
yarn dev
Visit your localhost, click the designated button—and that’s the whole science behind it!
Now, your sent email should appear in the testing inbox, where you can:
- Preview how it looks in plain-text or HTML on different devices.
- Check your email in source HTML code to see if there are any errors.
- Inspect if your HTML email is rendered properly by different email clients (e.g., Gmail, Outlook, Yahoo, etc.)
- Get some useful tech details like SMTP transaction and email header info.
- Use the detailed list of spam points to fix my emails, avoid spam filters, and prevent email deliverability issues once your app moves to production.
And much more! 📨
Note: The example above shows local testing. However, you can also test the sending functionality and your emails from the deployed app and see how it behaves in production. For this, I recommend making and deploying a separate environment with your fake SMTP credentials, since this is a general practice most teams follow.
Wrapping up
Overall, the scalability and simplicity offered by Vercel Functions paired with Mailtrap’s SMTP service make this solution production-ready.
By using the setup from this article, you can confidently send transactional emails from your Next.js app—no need to maintain a separate backend server.
Feel like further expanding your development skills? If so, be sure to check out our blog, where you can find other related articles such as: