Easy Way to Send Emails in a Next.js Application with MailFlash
Sending email in a Next.js application is easiest with a REST email API called from the server. You verify a sending domain, keep an API key in an environment variable, and make one HTTP request from a Server Action or Route Handler. No mail server, no SMTP configuration and no extra dependencies are required.
This guide walks through the full setup with MailFlash: connecting your domain, sending your first email, building a contact form, receiving delivery and inbound events with webhooks, and avoiding the mistakes that send mail to spam. Every code sample uses the App Router and TypeScript, and works on Next.js 14, 15 and newer.
In this guide
- What transactional email is and why it needs an API
- How the MailFlash API works
- Step-by-step setup (domain, API key, helper function)
- Sending from a Server Action and a Route Handler
- A working contact form with safe HTML
- Webhooks and inbound email
- Security, troubleshooting and deliverability
- FAQ
What Is Transactional Email and Why Does Next.js Need an API?
Transactional emails are messages triggered by something a user does: a welcome email after sign-up, a password reset link, an order receipt, a contact form confirmation or a magic login link. People expect them immediately, so delivery speed and inbox placement matter more than design.
You could send these through an SMTP library such as Nodemailer, but that is awkward in Next.js:
- The Edge runtime has no raw TCP sockets, so SMTP only works in the Node runtime.
- Serverless functions often open a fresh connection on every invocation, which adds latency.
- You manage ports, TLS settings and credentials yourself.
A REST API removes those problems. It is a normal fetch request over HTTPS, so it runs in Server Actions, Route Handlers and Edge functions alike.
How the MailFlash Email API Works
MailFlash covers transactional delivery, custom domain SPF and DKIM verification, inbox creation and real-time webhooks. Every request is authenticated with your API key, sent either as an Authorization: Bearer header or as an x-api-key header.
| Task | Method and endpoint |
|---|---|
| Send an email | POST /api/emails |
| List sent or received emails | GET /api/emails?type=sent or type=received |
| Add a sending domain | POST /api/domains |
| Verify domain DNS | POST /api/domains/:domain/verify |
| Register a webhook | POST /api/webhooks |
A send request takes from, to, subject, and a text and/or html body. A successful response returns success: true, a messageId and a status of Enqueued.
Before You Start
- A Next.js app using the App Router
- Node.js 18 or newer
- A domain you control, so you can add DNS records
- A MailFlash account and API key
Step 1: Add and Verify Your Sending Domain
Email providers trust mail that proves it comes from the domain it claims. MailFlash checks SPF, DKIM and MX records for your domain before you send from it.
Add the domain:
curl -X POST https://mailflash.org/api/domains \
-H "Authorization: Bearer $MAILFLASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"yourdomain.com"}'
The response returns a Pending status and a dnsRecords list. Add each record at your DNS provider, wait for DNS to propagate, then verify:
curl -X POST https://mailflash.org/api/domains/yourdomain.com/verify \
-H "Authorization: Bearer $MAILFLASH_API_KEY"
A correct setup returns "status": "Verified" with spf, dkim and mx all true. If a check fails, our SPF, DKIM and DMARC setup guide explains each record and the DNS mistakes that cause failures. Once SPF and DKIM pass, adding a DMARC policy is a sensible next step.
Step 2: Store Your API Key Securely
Create an API key in your MailFlash dashboard and add it to .env.local in the root of your project:
MAILFLASH_API_KEY=your_api_key_here
Two rules keep the key safe:
- Never prefix it with
NEXT_PUBLIC_. That prefix bundles the value into browser JavaScript where every visitor can read it. - Add the same variable to your hosting provider's environment settings (for example, your Vercel project) before you deploy.
Restart next dev after editing .env.local, because Next.js reads environment files at startup.
Step 3: Create a Reusable Email Helper
Put the API call in one server-only file, so every part of your app sends email the same way. Install the server-only package so the build fails if a client component ever imports this module:
npm install server-only
// lib/mailflash.ts
import "server-only";
const MAILFLASH_URL = "https://mailflash.org/api/emails";
export type SendEmailInput = {
from: string;
to: string;
subject: string;
text?: string;
html?: string;
};
export type SendEmailResult = {
success: true;
messageId: string;
status: string;
};
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
const res = await fetch(MAILFLASH_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFLASH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.success) {
throw new Error(`MailFlash send failed with status ${res.status}`);
}
return data as SendEmailResult;
}
This helper adds a 10-second timeout so a slow request cannot hang a serverless function, and it throws a clear error for failed sends. Because every email goes through this one function, switching providers later means editing a single file.
Step 4: Send an Email from a Server Action
Server Actions run on the server, so your key never reaches the browser. They are the simplest option when a form or button triggers the email.
// app/actions.ts
"use server";
import { sendEmail } from "@/lib/mailflash";
export async function sendWelcomeEmail(formData: FormData) {
const to = String(formData.get("email") ?? "").trim();
if (!to.includes("@")) {
throw new Error("Enter a valid email address");
}
await sendEmail({
from: "[email protected]",
to,
subject: "Welcome aboard",
text: "Thanks for signing up.",
html: "<p>Thanks for signing up.</p>",
});
}
Attach it to a form:
// app/page.tsx
import { sendWelcomeEmail } from "./actions";
export default function Page() {
return (
<form action={sendWelcomeEmail}>
<input name="email" type="email" required />
<button type="submit">Send welcome email</button>
</form>
);
}
Always include a plain text version alongside html. Some mail clients show only text, and a text part helps spam filters treat the message as legitimate.
Step 5: Send an Email from a Route Handler
Use a Route Handler when another service, a mobile app or a client-side fetch needs to trigger an email.
// app/api/send/route.ts
import { NextResponse } from "next/server";
import { sendEmail } from "@/lib/mailflash";
export async function POST(request: Request) {
const { to, subject, html } = await request.json();
if (!to || !subject || !html) {
return NextResponse.json(
{ error: "to, subject and html are required" },
{ status: 400 }
);
}
try {
const result = await sendEmail({
from: "[email protected]",
to,
subject,
html,
});
return NextResponse.json({ messageId: result.messageId });
} catch {
return NextResponse.json({ error: "Could not send email" }, { status: 502 });
}
}
Protect this route with authentication and rate limiting. A public endpoint that sends mail to any address lets strangers use your domain to spam people, which damages your sender reputation.
Server Action or Route Handler: Which Should You Use?
| Server Action | Route Handler | |
|---|---|---|
| Triggered by | A form or button in your app | An HTTP request from anywhere |
| Setup | Least code | You define request and response handling |
| Good for | Sign-up, contact forms, in-app buttons | Webhooks, cron jobs, third-party services |
| Key stays on server | Yes | Yes |
Step 6: Build a Contact Form That Emails You
A contact form is one of the most common reasons to send email from Next.js. The important detail is escaping user input before putting it in HTML, otherwise someone can inject markup into the message you receive.
// app/contact/actions.ts
"use server";
import { sendEmail } from "@/lib/mailflash";
function escapeHtml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
export async function sendContactMessage(formData: FormData) {
const name = String(formData.get("name") ?? "").trim();
const email = String(formData.get("email") ?? "").trim();
const message = String(formData.get("message") ?? "").trim();
if (!name || !email.includes("@") || !message) {
throw new Error("Please fill in all fields");
}
await sendEmail({
from: "[email protected]",
to: "[email protected]",
subject: `New message from ${name}`,
text: `From: ${name} <${email}>\n\n${message}`,
html: `<p><strong>${escapeHtml(name)}</strong> (${escapeHtml(email)})</p><p>${escapeHtml(message).replace(/\n/g, "<br>")}</p>`,
});
}
Notice that the from address is on your verified domain, while the visitor's address appears only in the body. Sending from an address you do not own fails authentication and is a common cause of spam-folder placement.
Step 7: Receive Delivery and Inbound Events with Webhooks
The Enqueued status means MailFlash accepted your message, not that it reached the inbox. Webhooks tell your app what happened next. MailFlash supports events such as email.delivered and email.received.
Register your endpoint:
curl -X POST https://mailflash.org/api/webhooks \
-H "Authorization: Bearer $MAILFLASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.com/api/mail-events","events":["email.received","email.delivered"]}'
Then add a Route Handler to receive them:
// app/api/mail-events/route.ts
export async function POST(request: Request) {
const event = await request.json();
// Log the raw payload first so you can see its exact shape
console.log("MailFlash event:", event);
return new Response("ok", { status: 200 });
}
Respond with a 200 quickly and move slow work, such as database updates, into the background. Use delivery events to confirm important messages like password resets, and stop sending to addresses that keep failing.
Step 8: Read Inbound Emails
MailFlash can also receive mail for your verified domain, which is useful for support inboxes and reply handling. Fetch received messages with one request:
curl "https://mailflash.org/api/emails?type=received&domain=yourdomain.com&limit=50" \
-H "Authorization: Bearer $MAILFLASH_API_KEY"
Use type=sent to list outgoing messages instead. Only owners of a verified domain can read its incoming mail.
Security Checklist for Sending Email in Next.js
- Keep the API key in an environment variable, never in source control.
- Call the email API only from the server: Server Actions, Route Handlers or server components.
- Authenticate and rate-limit any route that triggers email.
- Escape user-supplied text before placing it in HTML.
- Rotate the key immediately if it leaks.
- Send only from addresses on your verified domain.
Troubleshooting Common Errors
- Authentication fails: Check that the header is
Authorization: Bearer <key>(orx-api-key) and that the variable loaded. Restart the dev server after editing.env.local. - Email lands in spam: Confirm the domain shows as verified, the
fromaddress uses it, and you include atextversion. - Status stays
Enqueued: That is expected at send time. Confirm delivery with theemail.deliveredwebhook. - The key appears in the browser: Remove any
NEXT_PUBLIC_prefix, rotate the key and move the call to the server. - Requests hang in production: Keep the timeout in the helper and check your hosting platform's function time limit.
Email Deliverability Tips for Next.js Apps
- Verify SPF, DKIM and DMARC before sending real traffic.
- Send transactional and marketing email from separate domains or streams.
- Keep subject lines clear and avoid misleading text.
- Remove addresses that bounce or complain.
- Ramp volume up gradually on a new domain.
Our email deliverability checklist covers each point with practical detail.
Using a Different Email Provider
The helper pattern works with any HTTP email API. Change the URL, headers and body in lib/mailflash.ts and the rest of your app is untouched. If you are weighing options, read our comparison of Resend alternatives for transactional email.
FAQ: Sending Emails in Next.js
How do I send an email in a Next.js application?
Call an email API from the server using a Server Action or Route Handler. Store the API key in an environment variable and send a JSON request containing from, to, subject and a text or html body.
Can I send email from the client side in Next.js?
No. Calling an email API from the browser exposes your API key. Trigger the send from a Server Action or Route Handler instead.
Should I use SMTP or an API in Next.js?
An API is usually easier. SMTP needs the Node runtime and a new connection per serverless call, while a REST API works in Server Actions, Route Handlers and the Edge runtime.
Do I need to verify my domain to send email?
Yes. Verifying SPF and DKIM proves you own the domain and is the biggest factor in whether mail reaches the inbox.
How do I send a contact form email in Next.js?
Create a Server Action that reads the form data, validates it, escapes the text and calls your send function with a from address on your verified domain. The example in Step 6 shows the full pattern.
How do I receive emails in a Next.js app?
Register a webhook for the email.received event, or fetch received mail with GET /api/emails?type=received&domain=yourdomain.com.
Does this work on Vercel?
Yes. Add MAILFLASH_API_KEY to your project's environment variables and deploy as usual. Because the API call is a plain fetch, it runs on Vercel's serverless and Edge runtimes.
How do I know if an email was delivered?
The Enqueued response means the message was accepted. Subscribe to the email.delivered webhook to confirm delivery.
Conclusion
Sending email in Next.js comes down to a verified domain, a server-only API key, one small helper function and a webhook endpoint for events. Start with the Server Action example, test with an address you control, and add webhooks when you need to react to deliveries or inbound mail. With SPF and DKIM in place, your transactional email has the foundation it needs to reach the inbox.
