Install stuff we'll need:

1
2
3
  npm install react-email @react-email/components
  npm install -D tsup typescript
  npm install resend

Which installs so much stuff…

1
2
  ls -l node_modules | wc -l
  du -sh node_modules
     336
446M	node_modules

And gives us something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  {
      "type": "module",
      "main": "./dist/index.js",
      "module": "./dist/index.mjs",
      "types": "./dist/index.d.ts",
      "files": [
          "dist/**"
      ],
      "scripts": {
          "emaildev": "email dev",
          "build": "tsup --dts --external react",
          "dev": "tsup --dts --external react --watch",
          "clean": "rm -rf dist"
      },
      "dependencies": {
          "@react-email/components": "^0.0.22",
          "react-email": "^2.1.6",
          "resend": "^3.5.0"
      },
      "devDependencies": {
          "tsup": "^8.2.3",
          "typescript": "^5.5.4"
      }
  }

A quite little tsup.config.ts:

1
2
3
4
5
6
  import { defineConfig } from 'tsup'

  export default defineConfig({
      entry: ['render.tsx', 'resend.tsx'],
      target: 'es2024'
  })

Then we need to configure typescript, like so:

// tsconfig.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  {
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "node",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true,
    "jsx": "react-jsx",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "target": "es6",
  },
  "include": ["."],
  "exclude": ["dist", "build", "node_modules"]
}

Make the template

The npm run emaildev command looks for things in the emails folder, and it's really handle to have the preview while you edit.

1
  mkdir -p emails

I just played around with some different options of things to do, it works pretty well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  // emails/template.jsx
  import { Html,
           Heading,
           Container,
           Button } from "@react-email/components";
  import { CodeBlock, dracula } from "@react-email/code-block";

  const code = `export default async (req, res) => {
    try {
      const html = await renderAsync(
        EmailTemplate({ firstName: 'John' })
      );
      return NextResponse.json({ html });
    } catch (error) {
      return NextResponse.json({ error });
    }
  }`;

  export default function Email() {
    return (
      <Html>
          <Container>
              <Heading as="h1">This is an email</Heading>
            <Button href="https://willschenk.com"
                    style={{ background: "#000", color: "#fff", padding: "12px 20px" }}>
                Click me
            </Button>

             <CodeBlock
                 code={code}
                 lineNumbers
                 theme={dracula}
                 language="javascript"
             />

         </Container>
      </Html>
    );
  };

Test the templates

1
  npm run emaildev

This will open up a server on port 3000 that will let you live preview the changes that you make!

Render the template

1
2
3
4
5
6
  import Email from './emails/template';
  import { render } from '@react-email/components';

  console.log( render( <Email />, {
      pretty: true,
  } ) );
1
  node dist/render.cjs | htmlq --pretty

There's not really a big reason to do this other than to see how it works. We are going to use resend below to actually trigger the sending of the message.

Send with resend

Resend is a new service that makes it easier to send emails, and they were the ones that wrote react.email so of course the fit together nicely!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  import { Resend } from 'resend';
  import Email from './emails/template';

  if( process.env.RESEND_API === undefined ) {
      console.log( "Please set RESEND_API" );
      process.exit(1);
  }

  const resend = new Resend(process.env.RESEND_API);

  (async function() {
      const results = await resend.emails.send({
          from: 'onboarding@resend.dev',
          to:   'wschenk@gmail.com',
          subject: 'Test email',
          react: <Email />,
      });

      console.log( "Email sent" )
      console.log( results );
  })();

Testing

I'm pulling the API key out of 1password:

1
2
  # environment
  RESEND_API=op://Personal/Resend API/notesPlain

And then:

1
  op run --env-file environment -- node dist/resend.cjs
Email sent
{ data: { id: '6185f47f-f825-46f1-8557-a432ad2ccfc1' }, error: null }