How to Send Emails from Next.js (App Router)
A practical guide to sending transactional emails from Next.js using the SendPigeon SDK. Password resets, welcome emails, templates, and best practices.
Need to send emails from your Next.js app? This guide covers Server Actions, API routes, and best practices using the SendPigeon SDK.
Quick setup:
npm install sendpigeon- Add
SENDPIGEON_API_KEYto.env.local - Use Server Actions or API routes to send
Best for: Password resets, welcome emails, order confirmations, notifications.
Installation
npm install sendpigeon
Setup
Store your API key in .env.local:
SENDPIGEON_API_KEY=sp_live_your_key_here
Create a shared client:
// lib/email.ts
import { SendPigeon } from "sendpigeon";
export const pigeon = new SendPigeon(process.env.SENDPIGEON_API_KEY!);
Using Server Actions
Server Actions are the cleanest way to send emails in Next.js 14+. They run on the server, keeping your API key safe.
Password Reset
// app/actions/auth.ts
"use server";
import { pigeon } from "@/lib/email";
export async function requestPasswordReset(formData: FormData) {
const email = formData.get("email") as string;
// Generate reset token (your auth logic)
const token = crypto.randomUUID();
const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`;
await pigeon.send({
from: "auth@yourdomain.com",
to: email,
subject: "Reset your password",
html: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 1 hour.</p>
`,
});
return { success: true };
}
Use in your form:
// app/forgot-password/page.tsx
import { requestPasswordReset } from "@/app/actions/auth";
export default function ForgotPasswordPage() {
return (
<form action={requestPasswordReset}>
<input type="email" name="email" placeholder="your@email.com" required />
<button type="submit">Send Reset Link</button>
</form>
);
}
Welcome Email on Signup
// app/actions/signup.ts
"use server";
import { pigeon } from "@/lib/email";
export async function signupUser(formData: FormData) {
const email = formData.get("email") as string;
const name = formData.get("name") as string;
// Create user in database first
const user = await db.user.create({ data: { email, name } });
// Send welcome email
await pigeon.send({
from: "hello@yourdomain.com",
to: email,
subject: `Welcome, ${name}!`,
html: `
<h1>Welcome to MyApp!</h1>
<p>Thanks for signing up. Here's what you can do next:</p>
<ul>
<li>Complete your profile</li>
<li>Explore the dashboard</li>
<li>Invite your team</li>
</ul>
`,
});
return { success: true, userId: user.id };
}
Using Templates
For cleaner code, use stored templates with variables:
// Create template once (or via dashboard)
const template = await pigeon.templates.create({
name: "welcome-email",
subject: "Welcome, {{name}}!",
html: `
<h1>Welcome to MyApp, {{name}}!</h1>
<p>Your account is ready at {{email}}.</p>
<a href="{{dashboardUrl}}">Go to Dashboard</a>
`,
});
console.log(template.id); // "tpl_abc123" - use this when sending
// Use in your action (with the template ID, not name)
await pigeon.send({
from: "hello@yourdomain.com",
to: email,
templateId: template.id, // or "tpl_abc123"
variables: {
name: "Johan",
email: "johan@example.com",
dashboardUrl: "https://myapp.com/dashboard",
},
});
Store template IDs as constants or environment variables for reuse.
Using React Email
For type-safe, component-based emails, use React Email. Build emails with React, render to HTML, then send.
npm install @react-email/components @react-email/render
Create a reusable email component:
// emails/welcome.tsx
import { Html, Head, Body, Container, Text, Button } from "@react-email/components";
type WelcomeEmailProps = {
name: string;
dashboardUrl: string;
};
export function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: "sans-serif" }}>
<Container>
<Text>Welcome, {name}!</Text>
<Text>Your account is ready. Click below to get started.</Text>
<Button href={dashboardUrl}>Go to Dashboard</Button>
</Container>
</Body>
</Html>
);
}
Render and send in your Server Action:
// app/actions/signup.ts
"use server";
import { render } from "@react-email/render";
import { pigeon } from "@/lib/email";
import { WelcomeEmail } from "@/emails/welcome";
export async function signupUser(formData: FormData) {
const email = formData.get("email") as string;
const name = formData.get("name") as string;
const user = await db.user.create({ data: { email, name } });
const html = await render(
<WelcomeEmail name={name} dashboardUrl="https://myapp.com/dashboard" />
);
await pigeon.send({
from: "hello@yourdomain.com",
to: email,
subject: `Welcome, ${name}!`,
html,
});
return { success: true, userId: user.id };
}
React Email gives you type-safe props, component reuse, and live preview during development. Run npx react-email dev to preview your emails.
Using API Routes
For webhooks or client-triggered emails:
// app/api/send-invite/route.ts
import { pigeon } from "@/lib/email";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { email, inviterName, teamName } = await request.json();
const { data, error } = await pigeon.send({
from: "invites@yourdomain.com",
to: email,
subject: `${inviterName} invited you to ${teamName}`,
html: `<p>You've been invited to join ${teamName}. Click below to accept.</p>`,
});
if (error) {
return NextResponse.json({ error: error.message }, { status: error.status });
}
return NextResponse.json({ emailId: data.id });
}
Error Handling
The SDK returns { data, error } instead of throwing, making error handling straightforward:
import { pigeon } from "@/lib/email";
export async function sendWelcomeEmail(email: string) {
const { data, error } = await pigeon.send({
from: "hello@yourdomain.com",
to: email,
subject: "Welcome!",
html: "<h1>Welcome!</h1>",
});
if (error) {
console.error(`Email failed: ${error.message} (${error.status})`);
// Handle specific errors
if (error.status === 429) {
// Rate limited - queue for retry
}
return { success: false, error: error.message };
}
return { success: true, emailId: data.id };
}
Non-Blocking Emails
Don't let email failures break critical flows
Create the user first, then send the email. Log failures but don't fail the signup.
export async function signupUser(formData: FormData) {
const email = formData.get("email") as string;
// Critical: create user first
const user = await db.user.create({ data: { email } });
// Non-critical: send welcome email
const { error } = await pigeon.send({
from: "hello@yourdomain.com",
to: email,
subject: "Welcome!",
html: "<h1>Welcome!</h1>",
});
if (error) {
// Log but don't fail signup
console.error("Welcome email failed:", error.message);
}
return { success: true, userId: user.id };
}
Environment Setup
# .env.local (don't commit)
SENDPIGEON_API_KEY=sp_live_xxx
# .env.example (commit this)
SENDPIGEON_API_KEY=
Add .env.local to .gitignore. Never commit API keys.
Full Example: Contact Form
// app/actions/contact.ts
"use server";
import { pigeon } from "@/lib/email";
import { z } from "zod";
const contactSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const data = contactSchema.parse({
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
});
// Notify yourself
await pigeon.send({
from: "contact@yourdomain.com",
to: "you@yourdomain.com",
subject: `Contact form: ${data.name}`,
html: `
<p><strong>From:</strong> ${data.name} (${data.email})</p>
<p><strong>Message:</strong></p>
<p>${data.message}</p>
`,
replyTo: data.email,
});
// Confirm to sender
await pigeon.send({
from: "contact@yourdomain.com",
to: data.email,
subject: "We received your message",
html: `<p>Thanks for reaching out, ${data.name}. We'll get back to you soon.</p>`,
});
return { success: true };
}
Next Steps
- Set up DKIM, SPF, and DMARC for your sending domain
- Review our email deliverability checklist
- Check the SDK on GitHub for full API reference
- Browse our email templates for ready-to-use HTML
Other Frameworks
Using a different framework? We have guides for:
See all framework guides.