Setting Up SMTP Email with Nodemailer in Next.js
Learn how to configure a contact form with Gmail's free SMTP service using Nodemailer in your Next.js application
Why Gmail SMTP?
Gmail offers a free SMTP service that's perfect for personal websites and small projects. You can send up to 500 emails per day, which is more than enough for contact forms. No paid services needed!
Step 1: Install Nodemailer
First, install Nodemailer and its TypeScript types:
npm install nodemailer
npm install -D @types/nodemailerStep 2: Generate a Google App Password
Gmail requires an App Password instead of your regular password for SMTP access. Here's how to generate one:
Enable 2-Factor Authentication (Required)
- Go to Google Account Security
- Under "How you sign in to Google", click 2-Step Verification
- Follow the prompts to enable 2FA if not already enabled
Generate the App Password
- Go to Google App Passwords
- You may need to sign in again
- In the "App name" field, enter a name like
Portfolio Contact Form - Click Create
- Google will display a 16-character password (like
abcd efgh ijkl mnop) - Copy this password immediately — you won't be able to see it again!
Note: Remove the spaces when using the password. Use
abcdefghijklmnopinstead ofabcd efgh ijkl mnop.
Step 3: Set Up Environment Variables
Create a .env.local file in your project root:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=abcdefghijklmnop
CONTACT_EMAIL=your-email@gmail.com| Variable | Description |
|---|---|
SMTP_HOST | Gmail's SMTP server (smtp.gmail.com) |
SMTP_PORT | Port 587 for TLS |
SMTP_USER | Your Gmail address |
SMTP_PASS | The 16-character App Password (no spaces) |
CONTACT_EMAIL | Where you want to receive messages |
Step 4: Create the API Route
Create a new file at app/api/contact/route.ts:
import nodemailer from "nodemailer";
import { z } from "zod";
// Validate incoming data with Zod
const contactSchema = z.object({
name: z.string().trim().min(1, "Name is required").max(100),
email: z.string().trim().email("Invalid email").max(255),
message: z.string().trim().min(1, "Message is required").max(1000),
});
// Configure Gmail SMTP transporter
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
secure: false, // Use TLS
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export async function POST(req: Request) {
try {
const body = await req.json();
// Validate the request body
const result = contactSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: "Invalid form data" },
{ status: 400 }
);
}
const { name, email, message } = result.data;
// Send the email
await transporter.sendMail({
from: process.env.SMTP_USER,
to: process.env.CONTACT_EMAIL,
subject: `New message from ${name}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message.replace(/\n/g, "<br />")}</p>
`,
replyTo: email,
});
return Response.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Email sending error:", error);
return Response.json(
{ error: "Failed to send message" },
{ status: 500 }
);
}
}Step 5: Create the Contact Form Component
"use client";
import { useState } from "react";
export default function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("loading");
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
if (!response.ok) throw new Error("Failed to send");
setStatus("success");
setFormData({ name: "", email: "", message: "" });
} catch {
setStatus("error");
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
name="name"
placeholder="Your Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<input
type="email"
name="email"
placeholder="your@email.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<textarea
name="message"
placeholder="Your message..."
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
required
/>
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Sending..." : "Send Message"}
</button>
{status === "success" && <p>Message sent successfully!</p>}
{status === "error" && <p>Failed to send. Please try again.</p>}
</form>
);
}Step 6: Deploy to Production
When deploying to Vercel, Netlify, or any other platform, add the same environment variables:
- Go to your project settings
- Navigate to Environment Variables
- Add all four variables:
SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,CONTACT_EMAIL
Gmail SMTP Limits
Gmail's free SMTP has some limits to be aware of:
| Limit | Amount |
|---|---|
| Emails per day | 500 |
| Recipients per email | 500 |
| Email size | 25 MB |
For a personal portfolio contact form, these limits are more than sufficient.
Conclusion
Gmail's free SMTP service is perfect for personal websites and portfolios. With Nodemailer and a Google App Password, you can have a fully functional contact form without paying for any email service. Just remember to keep your App Password secure and never commit it to version control!