Skip to content
YourMail

Framework guide

Send email from Next.js

Why use this

Next.js gives you two server-side places to send from — a Server Action and a Route Handler. Both keep your API key on the server, which is the only thing that really matters here.

For example

A user submits your signup form; a Server Action sends them a welcome email before the page has finished re-rendering.

1. Install the SDK

The client is zero-dependency, so this adds nothing else to your bundle.

npm install yourmail

2. Put the key in the environment

Your API key is a server secret. Next.js exposes any variable prefixed NEXT_PUBLIC_ to the browser, so a key stored under that prefix is readable by every visitor and must be treated as compromised. Use a plain name.

# .env.local — server-side only.
# Do NOT prefix this with NEXT_PUBLIC_: that would ship your API key
# to every visitor's browser.
YOURMAIL_API_KEY=yourmail_...

3. Create the client once

One module-scope instance, imported only from server code. If you find yourself importing this from a component with "use client" at the top, stop — that is the mistake this file exists to prevent.

// lib/yourmail.ts
import { YourMail } from "yourmail";

// Constructed once at module scope. This module must only ever be imported
// from server code — a Server Action, a Route Handler, or a Server Component.
export const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!);

4. Send from a Server Action

The shortest path: a form posts straight to a server function, with no API route in between.

// app/contact/actions.ts
"use server";

import { ValidationError, YourMailError } from "yourmail";
import { yourmail } from "@/lib/yourmail";

export type SendState = { error?: string; sent?: boolean } | null;

export async function sendWelcome(_prev: SendState, formData: FormData): Promise<SendState> {
  const email = String(formData.get("email"));

  try {
    await yourmail.send({
      from: "Acme <hello@mail.acme.com>",
      to: email,
      subject: "Welcome aboard",
      html: "<p>Thanks for signing up.</p>",
      // Same key on a retry returns the original id instead of sending twice.
      idempotencyKey: `welcome:${email}`,
    });
    return { sent: true };
  } catch (err) {
    // The address came from a form, so a request the API calls invalid (a
    // malformed or suppressed address) is the user's to fix, not to retry.
    if (err instanceof ValidationError) {
      return { error: "We can't send to that address — please check it." };
    }
    if (err instanceof YourMailError) {
      // err.message names the problem; err.requestId is what to quote to support.
      console.error(err.type, err.message, err.requestId);
      return { error: "We couldn't send that email just now. Please try again later." };
    }
    throw err;
  }
}

The idempotencyKey is what makes a double-submitted form harmless: a repeat call with the same key returns the original message id rather than sending a second email.

5. Or send from a Route Handler

Use this when the caller is not your own form — a webhook from another service, a mobile client, a background job.

// app/api/send/route.ts
import { NextResponse } from "next/server";
import { yourmail } from "@/lib/yourmail";
import { ValidationError, RateLimitError, YourMailError } from "yourmail";

export async function POST(request: Request) {
  const { email } = await request.json();

  try {
    const { id } = await yourmail.send({
      from: "Acme <hello@mail.acme.com>",
      to: email,
      subject: "Welcome aboard",
      html: "<p>Thanks for signing up.</p>",
      idempotencyKey: `welcome:${email}`,
    });
    return NextResponse.json({ id });
  } catch (err) {
    // A bad address or a suppressed recipient — the caller's problem to fix,
    // and retrying it unchanged will fail identically.
    if (err instanceof ValidationError) {
      return NextResponse.json({ error: err.message }, { status: 422 });
    }
    // Over quota. Worth retrying later; not worth retrying now.
    if (err instanceof RateLimitError) {
      return NextResponse.json({ error: "Try again shortly." }, { status: 429 });
    }
    // Anything else the API refused — an unverified sender domain, an account
    // still in review, our side failing — is a problem with YOUR setup, not the
    // caller's request. Log it with its request id and answer 503.
    if (err instanceof YourMailError) {
      console.error(err.type, err.message, err.requestId);
      return NextResponse.json({ error: "Email is temporarily unavailable." }, { status: 503 });
    }
    throw err;
  }
}

Note which errors are caught and how. A ValidationError means the request itself is wrong and will fail the same way forever, so it becomes a 422 rather than a retry. The last branch matters most in your first week: an unverified from domain or an account still in review arrives as a plain YourMailError, and without it that becomes an unexplained 500. Every error type is listed in the errors reference.

Before it works in production

The from address has to be on a domain you have verified — verify a domain first. Before you get that far you can send from the shared test address no-reply@yourmail.dev, which delivers to your own inbox and needs no DNS at all.

Switching from another provider?

Last updated