Templates
React Email components, the shared EmailLayout, the pnpm email:dev preview loop, and developing with dry-run instead of a real provider key.
Templates are the visible half of the pipeline — the React components the worker renders into HTML. This page covers where they live, the shared layout that keeps them looking like one family, and the two ways you iterate on them without ever touching a queue.
React Email
Templates are React Email components in the top-level emails/ directory. Every template renders its content inside the shared EmailLayout (logo, brand accent strip, header, body, footer) so all emails look like one family. Here's the OTP template — short, prop-driven, wrapped in the layout:
export const OtpEmail = ({ firstName, otp, expiresInMinutes }: OtpEmailProps) => (
<EmailLayout preview={`Your verification code is ${otp}`} title="Verification Code">
<Text>Hi {firstName},</Text>
{/* OTP code in a styled box */}
<Section className="text-center my-[28px] bg-[#f0f4fa] rounded-[8px] ...">
<Text className="text-[32px] font-bold tracking-[6px] text-[#4169B2]">{otp}</Text>
</Section>
<Text>This code expires in {expiresInMinutes} minutes. {/* ... */}</Text>
</EmailLayout>
);
OtpEmail.PreviewProps = { firstName: 'John', otp: '482916', expiresInMinutes: 10 } as OtpEmailProps;The component is plain props in, JSX out. It is never rendered to HTML in the template file itself — the worker passes it as a react element straight to Resend, which does the rendering. The emails/ folder has its own tsconfig.json that sets "jsx": "react-jsx" so these .tsx files compile on their own.
Preview props
That PreviewProps static at the bottom of the file is sample data for the preview server. It's the realistic-looking input a designer would want to see the template against — a believable name, a six-digit code, a sensible expiry. Give every template a PreviewProps static so it has something to render with no live job behind it.
The preview loop
Run the preview server and you get a live, hot-reloading view of every template, with no queue, worker, or Redis involved — the fast loop for building a template:
pnpm email:devOpen the served URL, pick a template from the sidebar, and it renders using that component's PreviewProps. Edit the .tsx, save, and the preview updates instantly. This is where you do the actual design work — never by sending real emails.
Dry-run
The other way to exercise a template is to run the full pipeline with no provider key. ResendService only builds a real client when RESEND_API_KEY is set; with no key it short-circuits, logs a line, and returns:
async sendEmail(params): Promise<void> {
if (!this.client) {
this.logger.log(`[DRY RUN] Email to=${JSON.stringify(params.to)} subject="${params.subject}"`);
return;
}
// ...real send + throw on error
}Leaving RESEND_API_KEY empty locally is the intended way to develop. You get the full produce → enqueue → consume → render path exercised, with the final send replaced by a log line. The three config keys (RESEND_API_KEY, RESEND_FROM_EMAIL, RESEND_FROM_NAME) come from the env schema — see Config & Environment for how that mapping works.
Dry-run is a feature, not a fallback
Dry-run is the intended local workflow, but it hides one class of bug: the transport short-circuits before Resend renders anything, so a template that would throw on real data sails through as a [DRY RUN] log. Always preview your template with pnpm email:dev and, before relying on it in production, send at least one real email with a key set.
Stubbed templates
Five of the seven EmailType values currently have placeholder templates in the processor — the contract knows about them, but the real design hasn't been built yet. Producing one of those will run the full pipeline and "succeed" with an empty render. The contract being satisfied doesn't mean the email is real: if you wire a producer for an existing type, confirm its template is fully built first.
Where to go next
Adding an Email
The step-by-step for a brand-new email type, including where the template fits in the four steps.
Sending Emails
The overview: the producer → queue → worker → Resend pipeline this template plugs into.
Config & Environment
Where RESEND_API_KEY and the from-address keys are defined and mapped.