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 { 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;
}
}// app/contact/page.tsx
"use client";
import { useActionState } from "react";
import { sendWelcome } from "./actions";
export default function ContactPage() {
const [state, action, pending] = useActionState(sendWelcome, null);
return (
<form action={action}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit" disabled={pending}>Send</button>
{state?.sent && <p role="status">Sent — check your inbox.</p>}
{state?.error && <p role="alert">{state.error}</p>}
</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, 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?
- AWS SES: YourMail vs AWS SES
- Resend: migration guide · YourMail vs Resend
- Postmark: migration guide · YourMail vs Postmark
- SendGrid: migration guide · YourMail vs SendGrid
Last updated