How to Send Email in Next.js
Send email in Next.js using Server Actions, API routes, and templates. Complete guide with TypeScript code examples, React Email, error handling, and local testing.
To send email in Next.js, install the SendPigeon SDK, create a Server Action or API route, and call pigeon.send() with your recipient, subject, and content. This guide walks through every approach with full TypeScript examples you can copy into your project.
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.
Install the Next.js Email SDK
npm install sendpigeon
Set Up Email in Your Next.js Project
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!);
Send Email with Next.js 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. Make sure you've set up DKIM, SPF, and DMARC on your sending domain first — otherwise your emails may land in spam.
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 };
}
Send Email with Templates in Next.js
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.
Send Email in Next.js with React Email Components
For type-safe, component-based emails, use React Email. Build emails with React, render to HTML, then send. You can also use our free visual email builder if you prefer a drag-and-drop approach.
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.
Send Email from Next.js 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 });
}
Handle Email Errors in Next.js
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: Send Email from a Next.js 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 };
}
Test Email Sending Locally in Next.js
Before deploying, test your emails locally to avoid sending to real inboxes. See our full guide on local email testing and email sandbox testing for more options.
npx @sendpigeon-sdk/cli dev
This starts a local email server that catches all outgoing emails. View them at localhost:4100.
See our local email testing guide for full setup.
Frequently Asked Questions
Can you send email from a Next.js client component?
No. Email sending requires an API key, which must stay on the server. Use a Server Action or API route — never expose your API key in client-side code. Server Actions are the recommended approach since Next.js 14.
Is Nodemailer or an email API better for Next.js?
An email API like SendPigeon is better for most use cases. Nodemailer requires managing SMTP connections, doesn't work in serverless/edge environments, and adds complexity around connection pooling. An API is a single HTTP call that works everywhere Next.js deploys — Vercel, Netlify, Docker, or self-hosted.
How do you send email in Next.js without a backend?
You still need server-side code, but Next.js provides this built-in. Server Actions and API routes run on the server even when deployed as a "frontend" app. There's no separate backend needed — just "use server" and you're ready to send.
Does SendPigeon work with Next.js Edge Runtime?
Yes. The SendPigeon SDK uses the Fetch API under the hood, so it works in both the Node.js and Edge runtimes. You can use it in middleware, edge API routes, and edge-rendered pages.
How do you send bulk email from Next.js?
Use the batch endpoint to send up to 100 emails per request. This is useful for notifications, digests, or any scenario where you need to email multiple recipients with different content.
const { data } = await pigeon.batch({
emails: users.map(user => ({
from: "hello@yourdomain.com",
to: user.email,
subject: `Hey ${user.name}`,
html: `<p>Your weekly digest...</p>`,
})),
});
Next Steps
- Set up DKIM, SPF, and DMARC for your sending domain
- Review our email deliverability checklist
- Build custom templates with our free visual email builder
- 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.