Back to blog
Next.jsEmailTutorialServer Actions

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.

SendPigeon TeamDecember 20, 20258 min read

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.

TL;DR

Quick setup:

  1. npm install sendpigeon
  2. Add SENDPIGEON_API_KEY to .env.local
  3. 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


Other Frameworks

Using a different framework? We have guides for:

See all framework guides.