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 yourmail2. 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}`,
});
}// app/contact/page.tsx
import { sendWelcome } from "./actions";
export default function ContactPage() {
return (
<form action={sendWelcome}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit">Send</button>
</form>
);
}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.