Prompt
Send Transactional Email in Next.js App Router
Latest observation
Below is a simple, production-friendly way to send transactional emails from a Next.js App Router app using a Route Handler and a mail provider like Resend (same pattern works for SendGrid, Nodemailer, etc.).
1) Install a mail library
Example with Resend:
npm install resend
2) Add environment variables
Create .env.local:
RESEND_API_KEY=your_api_key_here
FROM_EMAIL=Acme <no-reply@yourdomain.com>
In production, use a verified sender domain.
3) Create an email service
lib/email.ts
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail(to: string, name: string) {
return resend.emails.send({
from: process.env.FROM_EMAIL!,
to,
subject: "Welcome to Acme!",
html: `
<div style="font-family: Arial, sans-serif;">
<h1>Hi ${name},</h1>
<p>Thanks for signing up. We're happy to have you!</p>
</div>
`,
});
}
4) Create an App Router API route
app/api/send-email/route.ts
import { NextResponse } from "next/server";
import { sendWelcomeEmail } from "@/lib/email";
export async function POST(req: Request) {
try {
const body = await req.json();
const { email, name } = body;
if (!email || !name) {
return NextResponse.json(
{ error: "Missing email or name" },
{ status: 400 }
);
}
const result = await sendWelcomeEmail(email, name);
return NextResponse.json({ success: true, result });
} catch (error) {
console.error("Email send error:", error);
return NextResponse.json(
{ error: "Failed to send email" },
{ status: 500 }
);
}
}
5) Call it from your UI
Example form in a client component:
app/page.tsx
"use client";
import { useState } from "react";
export default function HomePage() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [status, setStatus] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("Sending...");
const res = await fetch("/api/send-email", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, name }),
});
const data = await res.json();
if (res.ok) {
setStatus("Email sent!");
} else {
setStatus(data.error || "Something went wrong");
}
};
return (
<main style={{ padding: 24 }}>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<br />
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<br />
<button type="submit">Send Email</button>
</form>
<p>{status}</p>
</main>
);
}
6) Important notes
Use server-side only
Keep email-sending logic in:
app/api/.../route.ts- or Server Actions
- or server-only utility modules
Do not expose API keys in client components.
For transactional emails
Typical use cases:
- welcome emails
- password reset
- order confirmations
- OTP / verification codes
Add validation and rate limiting
For public endpoints, add:
- input validation (
zod) - rate limiting
- CAPTCHA if needed
Alternative: Send from a Server Action
If you prefer not to create a route handler, you can send directly in a Server Action. This is nice for form submissions in App Router.
If you want, I can also give you:
- a Nodemailer version,
- a Server Action version, or
- a full password-reset email flow in Next.js App Router.
Observation history
Unlock observation history
Create a free Obsurfable account to unlock historical responses, model comparisons, and deeper brand intelligence.
How did Obsurfable measure this prompt?
Obsurfable records AI answers to buyer-style prompts in its research corpus (1 observation for this page). Metrics are distributions over observations, not a single static ranking.
Which AI systems does Obsurfable collect answers from?
OpenAI, ChatGPT, Google, Gemini, Google AI Mode, Anthropic, Claude, Perplexity, Grok, DeepSeek, Mistral, Copilot, and Meta AI.