Opt-in email built on React templates: write emails as components, preview them in the browser, and send them. It lives in a separate entry point — @prehoy/baguette/email — so the React/mail dependencies stay out of the HTTP core. A plain API never pulls them in.
bun add @prehoy/baguette/email react @react-email/render @react-email/components nodemailer
React, @react-email/render, and nodemailer are optional peer dependencies — install them only if you use email.
Templates
An email is a React component in emails/, plus optional preview sample props for the preview endpoint. Fetch data in your route handler and pass it as props (templates stay pure and previewable).
// emails/Welcome.tsx
import { Body, Container, Head, Heading, Html, Text } from "@react-email/components";
export default function Welcome({ name }: { name: string }) {
return (
<Html>
<Head />
<Body>
<Container>
<Heading>Welcome, {name} 🥖</Heading>
<Text>Thanks for signing up.</Text>
</Container>
</Body>
</Html>
);
}
export const preview = { name: "Ada" }; // used by the preview endpoint
Preview
serve({ emails: true }) mounts a preview at /api/emails (lists every template) and /api/emails/:name (renders it as HTML with its preview props) — open it in a browser while you design.
serve({ routesDir: "./api", emails: true });
// GET /api/emails -> { "templates": ["Welcome", ...] }
// GET /api/emails/Welcome -> rendered HTML
It's opt-in and shows sample data only, but gate it in production if you don't want templates exposed: emails: process.env.NODE_ENV !== "production".
Sending
import { sendEmail } from "@prehoy/baguette/email";
import Welcome from "../emails/Welcome";
await sendEmail({
to: user.email,
subject: "Welcome",
react: <Welcome name={user.name} />, // rendered to HTML + a plain-text alternative
});
sendEmail renders the React element (if given), drops empty recipients, and sends through a transport. The transport is chosen from env:
| Env | Transport |
|---|---|
RESEND_API_KEY | Resend (bun add resend) |
SMTP_URL | nodemailer, connection string |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS | nodemailer |
EMAIL_FROM is the default sender. Pass a transport as the second argument to override, or use renderEmail(<Welcome .../>) to get the HTML string and send it yourself.
Queueing (for volume)
sendEmail sends directly — simple and correct for transactional email. To take sending off the request path (retries, backpressure), use the ready-made email queue. Re-export it as a queue file to run the worker:
// queues/email.ts
export { emailQueue as default } from "@prehoy/baguette/email";
Then enqueue instead of sending inline — pre-render, since job data is JSON:
await emailQueue.add({ to, subject: "Welcome", html: await renderEmail(<Welcome name={name} />) });
See Queues for the full queue layer.