cd ..
nextjs

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

4 min read

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/nodemailer

Step 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)

  1. Go to Google Account Security
  2. Under "How you sign in to Google", click 2-Step Verification
  3. Follow the prompts to enable 2FA if not already enabled

Generate the App Password

  1. Go to Google App Passwords
  2. You may need to sign in again
  3. In the "App name" field, enter a name like Portfolio Contact Form
  4. Click Create
  5. Google will display a 16-character password (like abcd efgh ijkl mnop)
  6. Copy this password immediately — you won't be able to see it again!

Note: Remove the spaces when using the password. Use abcdefghijklmnop instead of abcd 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
VariableDescription
SMTP_HOSTGmail's SMTP server (smtp.gmail.com)
SMTP_PORTPort 587 for TLS
SMTP_USERYour Gmail address
SMTP_PASSThe 16-character App Password (no spaces)
CONTACT_EMAILWhere 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:

  1. Go to your project settings
  2. Navigate to Environment Variables
  3. 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:

LimitAmount
Emails per day500
Recipients per email500
Email size25 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!

More to Read