Postmark has a real template system. You design a template in the dashboard, call sendEmailWithTemplate with an alias and a model, and Postmark fills it in. It also ships good starter templates.
The other option: write templates as React Email components, render them to HTML and text in your app, and send the result with sendEmail.
Both work. They put the template in different places, and that decides who can change it and how.
Postmark templates
await client.sendEmailWithTemplate({
From: from,
To: to,
TemplateAlias: "receipt",
TemplateModel: { reference, total, lines },
MessageStream: "outbound",
});
Pros:
- Non-developers can edit copy without a deploy.
- Layouts are shared across templates.
- Postmark can validate and preview with test data.
Costs:
- The template is not in your repo. No diff, no review, no history next to the code that fills it.
- The model is untyped. Rename
totaltoamountin code and the template renders an empty cell. Nothing fails. - Environments drift. Staging and production servers each hold their own copy. Someone fixes a typo in one.
- Logic lives in Mustachio. Loops and conditionals are fine. Currency formatting by locale is not.
React Email in the app
await sendEmail({
to,
subject: `Receipt ${reference}`,
react: ReceiptEmail({ reference, paidAt, currency, lines, total, locale }),
});
sendEmail renders the component with @react-email/render twice: once to HTML, once with plainText: true. Both go to Postmark as HtmlBody and TextBody.
Pros:
- Typed props. A missing field is a compile error.
- Reviewed like code. A copy change is a pull request.
- One source for every environment.
- Real formatting.
Intl.NumberFormatandIntl.DateTimeFormatwith the recipient's locale. - Local preview with
email dev(thereact-emailCLI) and realisticPreviewProps.
Costs:
- A copy change needs a deploy.
- Rendering costs a few milliseconds per send. Render once per message, not per recipient.
How to choose
- Engineers own the emails, and the emails carry money, links or tokens: render in the app. Receipts and sign-in links should never break silently because a model field was renamed.
- Marketing owns the copy and changes it weekly: Postmark templates can make sense, usually on a broadcast stream.
- Mixing is fine. Keep each template in exactly one place. A receipt that exists both as a component and as a Postmark template will drift.
If you use Postmark templates anyway
- Keep the aliases in one constants file.
- Type the model yourself:
satisfies ReceiptModelat each call site. - Push templates from the repo with the Templates API (
/templates/pushcopies between servers) instead of editing each server by hand. - Always send a text body. Postmark templates support a text version. Fill it.