How to Send Emails in Remix (2026 Guide)

Remix runs server code in action and loader functions. Email sending happens in actions (for form submissions) or resource routes (for API endpoints). API keys stay on the server, and you get progressive enhancement for free.
This guide covers both patterns with working examples.
Pick a Provider
- Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one SDK. Native Stripe integration.
- Resend is developer-friendly. They have one-off broadcast campaigns but no automations or sequences.
- SendGrid is the enterprise option. Good for high volume.
Install
npm install sequenzynpm install resendnpm install @sendgrid/mailAdd API key to .env and create a shared client:
import Sequenzy from "sequenzy";
export const sequenzy = new Sequenzy();
// Reads SEQUENZY_API_KEY from env automaticallyimport { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
export { sgMail };The .server.ts suffix ensures this code never runs in the browser.
Send from an Action
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { sequenzy } from "~/lib/email.server";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
const message = formData.get("message") as string;
if (!email || !message) {
return json({ error: "All fields required" }, { status: 400 });
}
try {
await sequenzy.transactional.send({
to: "you@yourcompany.com",
subject: `Contact from ${email}`,
body: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
return json({ success: true });
} catch {
return json({ error: "Failed to send" }, { status: 500 });
}
}
export default function Contact() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const sending = navigation.state === "submitting";
return (
<Form method="post">
<input name="email" type="email" placeholder="Your email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={sending}>
{sending ? "Sending..." : "Send"}
</button>
{actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
{actionData?.success && <p style={{ color: "green" }}>Sent!</p>}
</Form>
);
}import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { resend } from "~/lib/email.server";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
const message = formData.get("message") as string;
if (!email || !message) {
return json({ error: "All fields required" }, { status: 400 });
}
const { error } = await resend.emails.send({
from: "Contact <noreply@yourdomain.com>",
to: "you@yourcompany.com",
subject: `Contact from ${email}`,
html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
if (error) {
return json({ error: "Failed to send" }, { status: 500 });
}
return json({ success: true });
}
export default function Contact() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const sending = navigation.state === "submitting";
return (
<Form method="post">
<input name="email" type="email" placeholder="Your email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={sending}>
{sending ? "Sending..." : "Send"}
</button>
{actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
{actionData?.success && <p style={{ color: "green" }}>Sent!</p>}
</Form>
);
}import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { sgMail } from "~/lib/email.server";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get("email") as string;
const message = formData.get("message") as string;
if (!email || !message) {
return json({ error: "All fields required" }, { status: 400 });
}
try {
await sgMail.send({
to: "you@yourcompany.com",
from: "noreply@yourdomain.com",
subject: `Contact from ${email}`,
html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
return json({ success: true });
} catch {
return json({ error: "Failed to send" }, { status: 500 });
}
}
export default function Contact() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const sending = navigation.state === "submitting";
return (
<Form method="post">
<input name="email" type="email" placeholder="Your email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={sending}>
{sending ? "Sending..." : "Send"}
</button>
{actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>}
{actionData?.success && <p style={{ color: "green" }}>Sent!</p>}
</Form>
);
}Forms work without JavaScript (progressive enhancement). When JS loads, submissions happen without a full page reload.
Resource Routes (API Endpoints)
For webhooks or programmatic email sending, use resource routes (no UI component):
// app/routes/api.send-email.ts
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { sequenzy } from "~/lib/email.server";
export async function action({ request }: ActionFunctionArgs) {
const { to, subject, body } = await request.json();
const result = await sequenzy.transactional.send({ to, subject, body });
return json(result);
}Going to Production
1. Verify Your Domain
Add SPF, DKIM, DMARC DNS records.
2. Use .server.ts Files
The .server.ts suffix guarantees code only runs on the server. Use it for all email-related modules.
3. Progressive Enhancement
Remix forms work without JavaScript. Your email features work even if the client bundle fails to load.
Beyond Transactional
Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration for SaaS.
Wrapping Up
- Action functions for form-based email sending
- Resource routes for API-style endpoints
.server.tsfor server-only email modules- Progressive enhancement for reliable forms
Pick your provider, copy the patterns, and start sending.