Send Email with Next.js: A Safe Server-Side Setup
Send email from Next.js with a server-only MailBlastr helper, stable idempotency keys, authorized recipients and delivery-event tracking.
TL;DR
- Send email from trusted Next.js server code and keep the MailBlastr API key out of browser bundles.
- Derive the recipient and message content from an authorized business action, not arbitrary public form fields.
- Use a stable idempotency key for one logical message so an uncertain request can be retried safely.
- Treat an API acceptance as the start of delivery tracking, and use events to learn what happened afterward.
Choose the server boundary
A Next.js application can send email from a Route Handler, Server Action or background worker. Choose the place that owns the business action. An order receipt belongs after the application has verified the order and the user's right to access it. A public endpoint that accepts any recipient and HTML body is an open sending surface, not a complete integration.
Next.js Route Handlers use a route.ts or route.js file under the App Router and support standard request and response APIs. The framework supplies the HTTP boundary; your application still supplies authentication, authorization, validation and abuse controls.
The helper below is deliberately server-only. Call it from your existing authorized order workflow or durable job. It is not a browser component and does not create an unauthenticated public sending route.
Prepare the sending configuration
Create a MailBlastr key with the sending permissions your workflow needs, and verify a sending domain that you control. Store the key as MAILBLASTR_API_KEY and the sender address as MAILBLASTR_FROM in the server environment. Do not use a NEXT_PUBLIC_ prefix for either credential configuration or any other secret.
The sender address must belong to the verified domain. For development, use MailBlastr's documented simulator or a mailbox you control. Placeholder domains in examples are not real delivery destinations.
Read the current send-email reference for optional fields and limits. This example keeps the payload small and uses the HTTPS API directly, so it does not depend on a particular SDK version.
Add a server-only receipt helper
Save this as lib/send-order-receipt.ts. The caller supplies an already-authorized order event and an already-validated recipient. The message is plain text to keep the example focused and avoid interpolating untrusted values into HTML.
import 'server-only';
type ReceiptInput = {
orderEventId: string;
recipient: string;
orderReference: string;
};
export async function sendOrderReceipt(input: ReceiptInput) {
const apiKey = process.env.MAILBLASTR_API_KEY;
const from = process.env.MAILBLASTR_FROM;
if (!apiKey || !from) throw new Error('Email configuration is missing');
if (!/^[A-Za-z0-9_-]{1,100}$/.test(input.orderEventId)) {
throw new Error('A stable order event identifier is required');
}
if (!input.recipient || /[\r\n]/.test(input.recipient)) {
throw new Error('A validated recipient is required');
}
const response = await fetch('https://www.mailblastr.com/api/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `order-receipt/${input.orderEventId}`,
},
body: JSON.stringify({
from,
to: [input.recipient],
subject: 'Your order receipt',
text: `We received your order. Reference: ${input.orderReference}`,
}),
signal: AbortSignal.timeout(15000),
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`Email request was rejected (${response.status})`);
}
const result: unknown = await response.json();
if (!result || typeof result !== 'object' ||
!('id' in result) || typeof result.id !== 'string') {
throw new Error('Email response did not contain a message identifier');
}
return { emailId: result.id };
}The lightweight checks inside the helper are not a replacement for your application's input validation or access control. In particular, do not let an unauthenticated request choose someone else's order ID or recipient. Load those values from the order after checking the caller's access.
Connect it to durable application state
Store the business event before sending. A common design writes an order update and an email-outbox row in the same database transaction. A worker claims the outbox row, calls the helper and records the returned email ID.
If the network times out, the provider may already have accepted the request. Retry the same logical message with the same event ID and unchanged payload. Generating a fresh idempotency key for each attempt defeats duplicate protection. If the actual message changes, represent that as a new deliberate business event.
The helper does not implement a retry loop. Put retry policy in the durable worker where you can distinguish configuration or validation failures from transient conditions, respect provider responses and cap attempts. Avoid delaying an HTTP request indefinitely while repeatedly trying to send.
Verify more than the happy path
Test missing configuration, an unauthorized order request, an invalid sender domain, a normal accepted message and the same logical request repeated. Confirm that the server key never appears in rendered HTML, client JavaScript or browser network requests.
Then connect email webhooks to the stored email ID. Show users a truthful state such as queued, accepted or delivery failed. An accepted API request is not proof that the email reached the inbox or was read.
For HTML receipts, use a template system that escapes dynamic values and provide a readable plain-text version. Keep financial totals and order details sourced from your server-side order record rather than trusting a submitted form.
Common questions
Can I send directly from a client component?
The client should request an authorized application action. The actual provider call and API key belong on the server.
Should email failure roll back a completed order?
Usually the order and its notification have separate outcomes. Persist the order and a retryable notification job, then expose delivery problems to support without charging the customer again.
Why was the request accepted but no message arrived?
Inspect the stored email ID and later delivery events. For accepted mail that lands in spam, use the spam troubleshooting guide.