Nodemailer Setup

Used in this guide:
Next.js 15.3.3
Tailwind CSS 4
Nodemailer 7.0.3

Reason:

Implement a secure email-sending functionality using Nodemailer and Gmail SMTP for contact forms.

Setup Google SMTP

First, go to Google Account Security and enable 2-steps verification. Once that's done, type “app password” into the search bar at the top and select "App passwords (Security)" from the results. Enter a name for your app and click Create. Google will generate a 16-character password that you'll need to set up Nodemailer. Make sure to save this password somewhere safe, as it won’t be shown again.


Install Nodemailer

To use Nodemailer in your Next.js project, install it along with its type definitions:

npm install nodemailer
npm install @types/nodemailer

Prepare environment variables

Create a file called .env.local in the root directory and enter your email and Google App Password accordingly:


root/.env.local
# put your app password from Google Security -> App Password here
SMTP_PASSWORD=your_google_app_password
 
# put your email here (same email used to setup App Password)
SMTP_EMAIL=your_email
 
# don't change anything here
SMTP_HOST=smtp.gmail.com

Create API route

This file sets up a POST API route to handle contact form submissions using Nodemailer. It reads the user's name, email, and message from the request body, then sends an email via Gmail SMTP using environment variables that we created in the previous step for authentication.

app/api/util/send-email/route.ts
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";
 
export async function POST(request: Request) {
    const { name, email, message } = await request.json();
 
    const transporter = nodemailer.createTransport({
        host: process.env.SMTP_HOST,
        port: 587,
        secure: false,
        auth: {
            user: process.env.SMTP_EMAIL,
            pass: process.env.SMTP_PASSWORD,
        }
    });
 
    try {
        const info = await transporter.sendMail({
            from: `${name}: <${email}>`,
            to: process.env.SMTP_EMAIL,
            subject: "Email from my website",
            text: `You've received a new message from ${name} (${email}):\n\n${message}`,
        });
 
        console.log("Email sent: %s", info.messageId);
        return NextResponse.json({ message: "Email sent successfully" });
    } catch (error) {
        console.error("Error sending email: ", error);
        return NextResponse.json({ message: "Error sending email."}, { status: 500 });
    }
}

Create a form component

This component renders a contact form and sends user input (name, email, message) to the /api/util/send-email route on submit. It uses fetch to POST the form data as JSON and displays a status message based on the response. Input fields are controlled with React useState, and basic styling is applied using Tailwind utility classes.

app/components/SendEmailForm.tsx
'use client'
import { useState } from "react";
 
const SendEmailForm = () => {
 
    const [name, setName] = useState("");
    const [email, setEmail] = useState("");
    const [message, setMessage] = useState("");
    const [status, setStatus] = useState("");
 
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setStatus("Sending the email...");
        try {
            const res = await fetch("/api/util/send-email", {
                method: 'POST',
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ name, email, message }),
            });
 
            if (res.ok) {
                setStatus("Email sent successfully");
                setName("");
                setEmail("");
                setMessage("");
            } else {
                setStatus("Failed to send email, please try again.");
            }
        } catch (error) {
            console.error("Error sending email: ", error);
            setStatus("Failed to send email, please try again.");
        }
    };
 
    return (
        <form onSubmit={handleSubmit} className="w-[600px] flex flex-col gap-[24px] p-[24px] bg-gray-600">
            <div className="flex gap-[24px] w-full">
                <input
                    type="text"
                    placeholder="Name"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    required
                    className="bg-gray-500 p-[8px] w-full max-w-[50%]"
                />
                <input
                    type="email"
                    placeholder="Email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    required
                    className="bg-gray-500 p-[8px] w-full max-w-[50%]"
                />
            </div>
            <textarea
                placeholder="Message.."
                value={message}
                onChange={(e) => setMessage(e.target.value)}
                required
                className="bg-gray-500 p-[8px] w-full min-h-[200px]"
            />
            { status && (
                <div className="w-full text-violet-300">{status}</div>
            )}
            <button
                type="submit"
                className="bg-violet-300 py-[6px] px-[24px]"
                >Send</button>
        </form>
    )
}
 
export default SendEmailForm;

Use the form anywhere

Now you can use the form anywhere in your app. You are all set so go ahead any try it out.

app/page.tsx
import SendEmailForm from "./components/SendEmailForm";
 
export default function Home() {
  	return (
		<div className="flex items-center justify-center w-full h-[60vh]">
			<SendEmailForm />
		</div>
	);
}

JKT

Stay focused, and the rest will follow

©Jakkrit Turner. All rights reserved