Prompt
I'm building a Next.js app and want a simple production-ready email setup.
Latest observation
A simple, production-ready email setup for a Next.js app is usually:
- Send emails from server-side only via a Route Handler / Server Action
- Use a transactional email provider like Resend, Postmark, SendGrid, or Mailgun
- Render emails as HTML with a reusable template component
- Protect the endpoint with validation + rate limiting
- Use environment variables for secrets
- Add logging and error handling
Recommended stack
If you want “simple and modern,” I’d suggest:
- Resend for sending
- React Email for templates
- Zod for validation
- Next.js Route Handler for the API endpoint
This is easy to maintain and works well in production.
1) Install dependencies
npm install resend zod
npm install @react-email/components react-email
If you’re using TypeScript, you already have the basics in Next.js.
2) Add environment variables
RESEND_API_KEY=your_resend_key
EMAIL_FROM="Your App <no-reply@yourdomain.com>"
EMAIL_TO=you@yourdomain.com
Make sure EMAIL_FROM uses a domain verified in your email provider.
3) Create the email template
Example: emails/contact-email.tsx
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
interface ContactEmailProps {
name: string;
email: string;
message: string;
}
export function ContactEmail({ name, email, message }: ContactEmailProps) {
return (
<Html>
<Head />
<Preview>New contact form submission</Preview>
<Body style={{ backgroundColor: "#f6f9fc", fontFamily: "Arial, sans-serif" }}>
<Container style={{ backgroundColor: "#ffffff", padding: "24px" }}>
<Section>
<Text><strong>Name:</strong> {name}</Text>
<Text><strong>Email:</strong> {email}</Text>
<Text><strong>Message:</strong></Text>
<Text>{message}</Text>
</Section>
</Container>
</Body>
</Html>
);
}
4) Create the API route
Example: app/api/contact/route.ts
import { Resend } from "resend";
import { z } from "zod";
import { ContactEmail } from "@/emails/contact-email";
import { render } from "@react-email/components";
const resend = new Resend(process.env.RESEND_API_KEY);
const schema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
message: z.string().min(1).max(5000),
});
export async function POST(req: Request) {
try {
const body = await req.json();
const parsed = schema.safeParse(body);
if (!parsed.success) {
return Response.json(
{ error: "Invalid input", issues: parsed.error.flatten() },
{ status: 400 }
);
}
const { name, email, message } = parsed.data;
const html = render(
ContactEmail({
name,
email,
message,
})
);
const result = await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: process.env.EMAIL_TO!,
replyTo: email,
subject: `New contact form message from ${name}`,
html,
});
return Response.json({ success: true, id: result.data?.id });
} catch (error) {
console.error("Email send error:", error);
return Response.json(
{ error: "Failed to send email" },
{ status: 500 }
);
}
}
5) Add a simple form on the client
Example: app/contact/page.tsx
"use client";
import { useState } from "react";
export default function ContactPage() {
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setStatus(null);
const formData = new FormData(e.currentTarget);
const payload = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.ok) {
setStatus("Message sent!");
e.currentTarget.reset();
} else {
setStatus(data.error ?? "Something went wrong");
}
setLoading(false);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={loading}>
{loading ? "Sending..." : "Send"}
</button>
{status && <p>{status}</p>}
</form>
);
}
Production tips
1) Add rate limiting
Prevent spam by limiting requests per IP. You can use:
- Upstash Rate Limit
- Vercel middleware
- Cloudflare
- a simple DB-backed limiter
2) Add bot protection
Use one of:
- CAPTCHA / Turnstile
- honeypot field
- invisible anti-bot checks
3) Verify your sender domain
This reduces spam filtering and improves deliverability.
4) Keep sending server-side
Never expose your API key in the browser.
5) Use a dedicated transactional inbox
Send contact form emails to a monitored address, not a personal inbox.
If you want an even simpler option
If you don’t want templates yet, you can just send plain text or a minimal HTML string. But for production, the setup above is a good balance of simple and robust.
If you want, I can also give you:
- a copy-paste full Next.js contact form setup,
- a server action version, or
- a Resend + Tailwind email template.