Emails¶
This starter kit separates email authoring, local delivery, inspection, and production delivery:
| Tool | Responsibility |
|---|---|
| React Email | Compose and preview HTML templates with React components |
| Nodemailer | Send application messages to the local SMTP server during development |
| MailDev | Capture those SMTP messages and show them in a browser inbox |
| Resend | Deliver messages to real recipients in production |
Application code calls sendEmail(); the helper selects Nodemailer or Resend from validated server configuration. A previewed template is not an email sent by the application. Inspect MailDev to verify the whole development flow.
Project structure¶
packages/
emails/
email.tsx # React Email template
mail/
package.json # MailDev development inbox
apps/server/src/lib/
email-client.ts # Development SMTP and production Resend adapter
Development URLs¶
The email tools use fixed localhost ports:
| Tool | Command | URL |
|---|---|---|
| React Email preview | pnpm dev:mail |
http://localhost:3002 |
| MailDev inbox | pnpm dev |
http://localhost:3003 |
React Email previews templates while you edit them. MailDev shows messages actually sent by the application during development.
The local SMTP server listens on localhost:3025.
Author and send¶
Create a template¶
Export a component from packages/emails/ with explicit props:
import {
Body,
Button,
Container,
Head,
Html,
Preview,
Text,
} from "react-email";
type WelcomeEmailProps = {
name: string;
loginUrl: string;
};
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to the app</Preview>
<Body>
<Container>
<Text>Welcome, {name}!</Text>
<Button href={loginUrl}>Sign in</Button>
</Container>
</Body>
</Html>
);
}
Use absolute URLs for links and images. Keep the props limited to values the template renders.
Preview templates¶
Run pnpm dev:mail, then open http://localhost:3002. The React Email development server reloads when a template changes.
The equivalent package-level command is:
Send an email¶
Import the provider-neutral helper from server code:
export type Email = {
from: string;
to: string | string[];
subject: string;
html: string;
text?: string;
};
export function sendEmail(email: Email, config: Config) {
if (config.nodeEnv === "production")
return new Resend(config.resendApiKey).emails.send(email);
return nodemailer.createTransport({ host: "127.0.0.1", port: 3025 }).sendMail(email);
}
sendEmail() accepts ordinary HTML and optional plain text, so a new feature does not need to know whether development uses Nodemailer or production uses Resend:
import { sendEmail } from "../lib/email-client.ts";
await sendEmail({
from: "Your App <onboarding@example.com>",
to: user.email,
subject: "Welcome to Your App",
html: "<p>Welcome to Your App!</p>",
text: "Welcome to Your App!",
}, config);
Validate recipients and template input before sending. Do not include secrets or raw request data in email logs.
The React Email workspace is the visual template authoring and preview environment. Keep templates there while designing them. The server helper deliberately accepts rendered HTML, which keeps delivery code small and also supports HTML produced by React Email, a Markdown renderer, or a simple string template.
Verify delivery¶
Inspect development messages¶
Outside production, sendEmail() uses SMTP at 127.0.0.1:3025. MailDev captures the message and displays it at http://localhost:3003. Nothing is delivered to the real recipient.
This lets integration work use realistic email content without requiring provider credentials or sending accidental messages.
Local development flow¶
- Run
pnpm devonce from the repository root. - Write server code that calls
sendEmail(). - Trigger that code from the application or an integration test.
- Open
http://localhost:3003. - Inspect the subject, recipients, HTML, text, and links.
No API key, external account, or manually started SMTP process is required for this flow.
Production delivery¶
When NODE_ENV=production, sendEmail() uses Resend. Set this secret in the deployment environment:
Configure and verify the sender domain in Resend before using it in the from field. Do not prefix the key with VITE_ or expose it to the browser.
Verify¶
Then:
- Open
http://localhost:3002and verify the template renders. - Trigger the application action that sends the message.
- Open
http://localhost:3003and inspect the received HTML and text. - Check links, subject, sender, recipient, and narrow-screen rendering.