DEV Community

Reuben Wedson
Reuben Wedson

Posted on

Integrating Resend with React Email in a Better T Stack Monorepo (Next JS & Hono backend with Better Auth)

This guide shows how to set up Resend + React Email inside a Better T Stack monorepo. The goal is to keep email logic isolated in a shared package while making it easy to use from your applications (web, server, authentication flows like password reset).

This setup works especially well with Better Auth, but it’s not tied to it — any backend or server action can use the same email package.


Project Structure Overview

The monorepo follows a typical Better T Stack layout:

apps/
  ├─ web
  └─ server

packages/
  ├─ config
  └─ transactional
Enter fullscreen mode Exit fullscreen mode
  • apps/ Application code (frontend and backend)
  • packages/ Shared libraries
  • packages/transactional/ Transactional email package (React Email + Resend)
  • packages/config/ Shared TypeScript configuration

This structure keeps email templates reusable, typed, and independent from application logic.


Shared Configuration Package (@project/config)

Before looking at emails, it’s important to understand the shared config package. This is what makes JSX, strict typing, and React Email work consistently across the monorepo.

Instead of duplicating tsconfig settings in every app and package, everything extends a single base configuration.

Why This Matters

Email templates:

  • Use JSX
  • Live in shared packages (not Next.js apps)
  • Are compiled in multiple environments

Without a shared config, you quickly run into:

  • JSX not being enabled in packages
  • Different module resolutions
  • Subtle build failures between apps

packages/config/package.json

{
  "name": "@project/config",
  "version": "0.0.0",
  "private": true
}
Enter fullscreen mode Exit fullscreen mode

This package is:

  • Private (not published)
  • Used only inside the monorepo
  • Referenced via workspace imports

packages/config/tsconfig.base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ESNext"],

    "verbatimModuleSyntax": true,
    "strict": true,
    "skipLibCheck": true,

    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,

    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "types": ["bun"],
    "jsx": "react-jsx"
  }
}
Enter fullscreen mode Exit fullscreen mode

Important:
The "jsx": "react-jsx" option is what allows React Email templates (.tsx) to compile correctly inside shared packages.


Using the Shared Config in Packages

Each package extends the base config.

Example for the transactional email package:

// packages/transactional/tsconfig.json
{
  "extends": "@project/config/tsconfig.base.json",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist",
    "composite": true
}}
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • JSX works everywhere
  • Strict typing is consistent
  • Bun, ESNext, and React Email all align

Transactional Email Package Setup

The transactional email logic lives in packages/transactional.

packages/transactional/package.json

{
  "name": "@project/transactional",
  "type": "module",
  "exports": {
    ".": {
      "default": "./src/index.tsx"
    },
    "./*": {
      "default": "./src/*.tsx"
    }
  },
  "devDependencies": {
    "@remedy/config": "workspace:*",
    "@types/react": "19.2.7",
    "typescript": "catalog:"
  },
  "dependencies": {
    "@react-email/components": "1.0.2",
    "react-email": "5.1.0",
    "resend": "6.6.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

This package:

  • Exposes email templates
  • Sends emails via Resend
  • Can be imported by any app in the monorepo

Creating Email Templates

Email templates are React components stored under:

packages/transactional/src/emails/
Enter fullscreen mode Exit fullscreen mode

Example: Password Reset Email - used react email notion template

import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Tailwind,
  Text,
} from "@react-email/components";

interface DefaultEmailProps {
  url?: string;
}

export const DefaultEmail = ({ url }: DefaultEmailProps) => (
  <Html>
    <Head />
    <Tailwind>
      <Body className="bg-white font-notion">
        <Preview>Reset your password</Preview>

        <Container className="px-3 mx-auto">
          <Heading className="text-[#333] text-[24px] my-10">
            Reset your password
          </Heading>

          <Link
            href={url}
            target="_blank"
            className="text-[#2754C5] text-[14px] underline mb-4 block"
          >
            Click here to reset your password
          </Link>

          <Text className="text-[#333] text-[14px] my-6">
            Or copy and paste this link into your browser:
          </Text>

          <code className="block p-4 bg-[#f4f4f4] rounded border text-[#333]">
            {url}
          </code>

          <Text className="text-[#ababab] text-[14px] mt-4">
            If you didn’t request a password reset, you can safely ignore this email.
          </Text>
        </Container>
      </Body>
    </Tailwind>
  </Html>
);
Enter fullscreen mode Exit fullscreen mode

Because this is just React:

  • Templates are reusable
  • Props are type-safe
  • Styling stays consistent

Sending Emails with Resend

The sending logic is centralized in the package entry point.

packages/transactional/src/index.tsx

import { Resend } from "resend";
import { DefaultEmail } from "./emails/default";

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

export const sendEmail = async ({
  to,
  subject,
  url,
}: {
  to: string;
  subject: string;
  url: string;
}) => {
  return resend.emails.send({
    from: process.env.EMAIL_FROM ?? "Acme <onboarding@resend.dev>",
    to,
    subject,
    react: <DefaultEmail url={url} />,
  });
};
Enter fullscreen mode Exit fullscreen mode

This keeps:

  • API keys out of app code
  • Email logic centralized
  • Templates decoupled from business logic

Using the Email Package in an App

1. Install the package

bun add @project/transactional
Enter fullscreen mode Exit fullscreen mode

2. Configure environment variables

RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <noreply@yourdomain.com>"
Enter fullscreen mode Exit fullscreen mode

3. Send an email

import { sendEmail } from "@remedy/transactional";

await sendEmail({
  to: "user@example.com",
  subject: "Reset your password",
  url: "https://yourapp.com/reset-password?token=abc123",
});
Enter fullscreen mode Exit fullscreen mode

This fits cleanly into:

  • Password reset flows
  • Email verification
  • Invitations
  • System notifications

Adding More Email Templates

To add another email (for example, a welcome email):

  1. Create a new file in emails/
  2. Export a React component
  3. Import and use it in your sender logic

You can either:

  • Add new helper functions per template, or
  • Build a single sendTransactionalEmail wrapper

Local Development & Preview

React Email provides a preview server:

cd packages/transactional
bunx email dev
Enter fullscreen mode Exit fullscreen mode

Open:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

This lets you iterate on templates without sending real emails.


Best Practices

  • Keep email logic out of app code
  • Use strict typing for template props
  • Reuse components (headers, footers, buttons)
  • Preview emails locally
  • Test sending in staging before production
  • Wrap sending logic with error handling

Deployment Notes

Before deploying:

  • Verify RESEND_API_KEY and EMAIL_FROM
  • Confirm sender/domain in Resend
  • Test email flows in staging

Final Thoughts

This setup gives you:

  • Clean separation of concerns
  • Typed, reusable email templates
  • Consistent builds across the monorepo
  • A strong foundation for auth-related emails (including Better Auth)

The shared @project/config package is the key that makes everything work smoothly — especially JSX inside shared packages.

If you want next steps, we can:

  • Wire this directly into Better Auth password reset
  • Introduce shared ESLint/Biome config alongside @project/config

This is production-ready foundation.

Top comments (0)