Prompt
How to send transactional email in Next.js App Router
Latest observation
You said:
How to send transactional email in Next.js App Router
ChatGPT said:
The recommended way to send transactional emails in Next.js App Router is to use a dedicated email provider such as Resend, SendGrid, or Amazon SES, and trigger it from a server-side Route Handler (app/api/.../route.ts) or a Server Action.
For most Next.js projects, Resend + React Email is a straightforward setup. It works well for welcome emails, email verification, password resets, order confirmations, and contact form notifications.
1. Install the dependencies
Bash
npm install resend @react-email/components
Create a Resend account and verify a domain you control so you can send from an address such as notifications@yourdomain.com.
2. Configure environment variables
Add these to your .env.local file:
envRESEND_API_KEY=re_your_api_key EMAIL_FROM=My App <notifications@yourdomain.com>
Keep your API key server-side. Never prefix it with NEXT_PUBLIC_ or expose it to browser code.
3. Create a reusable email client
Create lib/email.ts:
TypeScript
import "server-only";
import { Resend } from "resend";
export const resend = new Resend(
process.env.RESEND_API_KEY
);
The server-only package is typically available in Next.js projects and helps prevent accidental imports into Client Components.
4. Create an API Route Handler
Create app/api/emails/welcome/route.ts:
TypeScript
import { NextResponse } from "next/server";
import { resend } from "@/lib/email";
export async function POST(request: Request) {
try {
const body = await request.json();
const { email, name } = body;
if (
typeof email !== "string" ||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ||
typeof name !== "string" ||
name.trim().length === 0 ||
name.length > 100
) {
return NextResponse.json(
{ error: "Invalid email or name" },
{ status: 400 }
);
}
const { data, error } = await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: [email],
subject: "Welcome to My App!",
text: `Hi ${name}, welcome to My App!`,
html: `
<h1>Welcome, ${escapeHtml(name)}!</h1>
<p>Thanks for joining My App.</p>
`,
});
if (error) {
console.error("Email provider error:", error);
return NextResponse.json(
{ error: "Failed to send email" },
{ status: 502 }
);
}
return NextResponse.json(
{ success: true, id: data?.id },
{ status: 200 }
);
} catch {
return NextResponse.json(
{ error: "Invalid request or email failure" },
{ status: 400 }
);
}
}
function escapeHtml(value: string) {
return value.replace(/[&<>"']/g, (char) => {
const entities: Record<string, string> = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
return entities[char];
});
}
This follows Resend's supported Next.js App Router integration pattern. Resend+1
A few details matter here:
POSThandles the email request on the server.- Input validation prevents malformed requests.
- HTML escaping prevents user-supplied names from injecting markup into the email.
- The API key and email provider stay on the server.
- A successful API response means the provider accepted the request, not necessarily that the recipient received the message.
Important: this example demonstrates the mechanics. Before exposing this endpoint publicly, add rate limiting, appropriate authorization, and abuse protection. For a welcome email, ideally trigger it from your trusted signup or account-creation flow rather than allowing arbitrary clients to send emails to arbitrary recipients.
5. Call the endpoint from your frontend
For example, from a Client Component:
TypeScript
"use client";
import { useState } from "react";
export function SignupEmailButton() {
const [status, setStatus] = useState("");
async function sendEmail() {
setStatus("Sending...");
try {
const response = await fetch("/api/emails/welcome", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "user@example.com",
name: "Alex",
}),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error);
}
setStatus("Email request accepted!");
} catch {
setStatus("Could not send email.");
}
}
return (
<div>
<button onClick={sendEmail}>
Send welcome email
</button>
<p>{status}</p>
</div>
);
}
This is useful for testing. In production, the recipient should normally come from the authenticated user's account, not an arbitrary email supplied by the browser.
6. Use React Email for production templates
For branded emails, create reusable React-based templates instead of embedding HTML strings in route handlers.
Install the template components:
Bash
npm install @react-email/components
Create emails/welcome.tsx:
TypeScript
import {
Html,
Head,
Body,
Container,
Heading,
Text,
} from "@react-email/components";
type WelcomeEmailProps = {
name: string;
};
export function WelcomeEmail({ name }: WelcomeEmailProps) {
return (
<Html lang="en">
<Head />
<Body style={{ fontFamily: "Arial, sans-serif" }}>
<Container>
<Heading>Welcome, {name}!</Heading>
<Text>
Thanks for creating an account with us.
</Text>
</Container>
</Body>
</Html>
);
}
Then, in your route handler, replace the text and html properties with:
TypeScript
import { WelcomeEmail } from "@/emails/welcome";
// Inside your POST handler:
const { data, error } = await resend.emails.send({
from: process.env.EMAIL_FROM!,
to: [email],
subject: "Welcome to My App!",
react: WelcomeEmail({ name }),
});
This keeps the email's presentation separate from the business logic. See the official Resend + Next.js guide for more examples. Resend+1
7. Production checklist
Before shipping transactional emails, make sure you have these covered:
- Domain authentication: Configure SPF, DKIM, and any recommended DMARC records with your email provider.
- Rate limiting: Prevent attackers from repeatedly triggering emails.
- Authorization: Only send account-related emails when the relevant action is legitimate.
- Templates: Use separate templates for verification, password resets, receipts, and notifications.
- Error handling: Log provider errors without exposing secrets or sensitive data.
- Retries and idempotency: Avoid duplicate emails when requests are retried.
- Delivery monitoring: Track bounces, complaints, and delivery events through provider webhooks.
- Environment variables: Configure your API key and sender address in your deployment environment as well as locally.
For password resets and email verification, use short-lived, single-use tokens and avoid revealing whether a particular account exists.
Which approach should you choose?
| Approach | Best for |
|---|---|
| Route Handler (app/api/.../route.ts) | API endpoints, frontend requests, external integrations |
| Server Action | Emails triggered by a form submission inside your Next.js app |
| Background job or queue | High-volume sending, retries, and reliable delivery workflows |
My recommendation: Start with Resend + a Route Handler + React Email. If the email is part of signup, password reset, payment, or order processing, trigger it from the corresponding trusted server-side business logic. For critical workflows, use a durable job queue or transactional outbox so an email isn't lost when a request or deployment fails.
Useful references: