Migrating from Postmark Templates to React Email

An attempt at reducing some platform or dependency risk in my product portfolio

Recently Postmark has gone through some changes after they were acquired and had a bit of drama recently. While I wasn’t affected because I only integrate using their API it did make me wonder how easy it would be to switch all my services over. Sending email via a different service is fairly trivial, but I do heavily depend on Postmark’s templating system at the moment, which other providers like Resend, SES do not have. While I don’t think Postmark is going anywhere soon, this has now become a bit too risky for me, so as Arvid Kahl would say, this needs an abstraction.

Thankfully Resend (note: this is not an endorsement for them, as I have exactly 0 experience so far with them, but they are a contender for my future emailing needs, see below) has an open-source package called React.Email.

What I envision is a intermediate server that does the templating and then I can plugin any ESP on the back. Looking at the docs I believe this is something I can easily achieve with React.Email. Let’s see.

Full code is available here: https://github.com/aduggleby/TemplateMailer-Example

Step 1. Install React.Email

First step is to run npx create-email@latest in a new directory. Then running npm install followed by npm run dev in the newly created directory will start up the email browser viewer.

Using the send button at the top you can even send yourself a test email without adding any api tokens. Neat, that was easy.

Step 2. Converting Postmark Template to Resend.Email Components

Ideally you use the React.Email component to make your emails from scratch and beautiful (maybe some inspiration from their components gallery) but at the end of the day we have other tasks to do, so how can we get the Postmark templates over into React.Email as quickly as possible.

Turns out ChatGPT is your friend.

I exported the layout HTML and welcome template HTML into seperate files and prompted ChatGPT as follows:

For the layout file

Convert the attached file into a JSX Layout component replacing the content section with the appropriate children call. Also inline all the styles defined in the header into the individual components using style object syntax.

The result of which you can find here: https://chatgpt.com/share/66ec0b2c-0710-8000-a6d4-d4fd0a9ea91c

For the template file

Convert the attached file into a JSX component called “Welcome” wrapped by the “Basic” layout component from “layouts/Basic”. Create an interface called “WelcomeProps” that defined the properties used in the layout but use camel casing instead of snake casing. Replace the tokens in the file with references to the destructured props object passed into the object.

The result of which you can find here: https://chatgpt.com/share/66ec0af6-3344-8000-bd43-ec490d338b6c

It’s needs some basic cleaning-up, Typescript is complaining about some styles and styles are duplicated across both files, but you can’t complain about the result too much IMHO for 5 minutes at most:

I’ve decided to mirror the Postmark setup with one folder per server and one template file (with layouts in a separate subdirectory).

Step 3. Add a sending API

Won’t bore you with Express/TSC setup, see the repository for that.

I’d like the API to work fairly similar to Postmark, i.e. support different server tokens, but use Bearer tokens instead of a custom header.

curl "https://pizza.hawaii/" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H 'Authorization: Bearer MYTOKEN123' \
  -d '{
  "from": "[email protected]",
  "to": "[email protected]",
  "templateAlias": "welcome",
  "templateModel": {
    ...
  }
}'

For that we’ll need to create a config.tsx file to setup tokens and templates.

import Welcome from './emails/postmark-migration/welcome'

export default {
	// The token to use for authorization
	MYTOKEN123: {
		// The email templates
		welcome: {
			email: Welcome,
			subject: 'Postmark Migration Test'
		}
	}
}

Each token has a set of templates which have a component to render and a subject.

The subject is probably the biggest difference to the Postmark way of things, as React.Mail doesn’t allow defining the subject in the template as far as I am aware, so we put it in the config, but it’s static, where-as Postmark does allow you to pass variables into the subject line.,

The core of the API implementation is fairly straightforward:

app.post("/", async (req, res) => {
  const authHeader = req.headers.authorization;
  const token = authHeader.split(" ")[1];

  const { from, to, templateAlias, templateModel } = req.body;

  var templateConfig = ((config as any)[token] as any)[templateAlias];
  var templateComponent = templateConfig.email;

  await client.sendEmail({
    From: from,
    To: to,
    Subject: templateConfig.subject,
    HtmlBody: await render(templateComponent(templateModel)),
  });

  return res
    .status(200)
    .json({ message: "Email with template sent successfully" });
});

For the actual sending (the client object) we’re just going to use the postmark client, but there’s many different options shown on the react.email website you could replace that with including of course the makers of react.email Resend which I may give a try in the future just to not tie myself to strongly to just one ESP.

const client = new ServerClient(process.env.POSTMARK_SERVER_TOKEN!);

Conclusion

There we go. We’ve abstracted the email templating part into a separate server component we control and can deploy to our favourite provider. I just deployed to Render and it worked great.

Some Improvement Ideas (now implemented)

  • Currently all emails are sent through a single ESP (in my example a single Postmark Server). Ideally you might have different accounts per token, which you could configure in the config file.

  • Subjects should ideally have a basic string interpolation, which can could most easily be done by providing a function in the config that takes the templateModel and produces the string.

Update: I just went ahead and added subject string interpolation and a basic ESP configuration mechanism.