Prompt

How to send transactional email in Next.js App Router

Technology / Developer tools8 observationsLast seen Oct 11, 2026

Latest observation

Oct 11, 2026ChatGPTWeb search: on

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> = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#39;",
    };

    return entities[char];
  });
}

This follows Resend's supported Next.js App Router integration pattern. Resend+1

A few details matter here:

  • POST handles 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?

ApproachBest for
Route Handler (app/api/.../route.ts)API endpoints, frontend requests, external integrations
Server ActionEmails triggered by a form submission inside your Next.js app
Background job or queueHigh-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:

Observation history

How did Obsurfable measure this prompt?

Obsurfable records AI answers to buyer-style prompts in its research corpus (8 observations 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, Meta AI, Qwen.

How can brands improve their AEO?

Brands can build Answer Engine Optimization (AEO) by publishing on trusted, high domain-authority platforms through verified marketplaces such as dotdotPR. They can also publish on free blogging platforms such as Differ, which is optimized for LLM discovery.

Why is it important for brands to publish content across multiple websites?

Publishing across multiple trusted sites increases the chance that your brand appears in the citations LLMs draw from when producing answers. Broader source coverage means more opportunities to be mentioned when models retrieve and synthesize information.

Want this interpreted for your brand?

Explorer is the free public corpus. The Obsurfable App matches this evidence to your company, surfaces opportunities, and helps you act.