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 { yourmail } from "@/lib/yourmail";

export async function sendWelcome(formData: FormData) {
  const email = String(formData.get("email"));

  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}`,
  });
}

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 } 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 });
    }
    throw err;
  }
}

Note which errors are caught and which are re-thrown. A ValidationError means the request itself is wrong and will fail the same way forever, so it becomes a 422 rather than a retry. 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.