Setting Up React Email in a Monorepo: Complete Guide (2026)
As your SaaS or startup grows, you'll accumulate dozens of email templates — onboarding, password resets, payment receipts, trial reminders, security alerts. Without structure, they scatter across repos and become hard to maintain.
A monorepo solves this: keep all your email templates in one place, share components across apps, and update them together.
This guide shows you how to set up React Email in a monorepo using npm workspaces (with a pnpm alternative), organized for real SaaS development.
What is a Monorepo and Why Use It for Email Templates?
A monorepo keeps multiple packages in a single repository — like having all your tools in one toolbox.
For React email templates, this means you can:
- ✅ Share components (header, footer, button) between transactional and marketing emails
- ✅ Keep all email code organized in one place
- ✅ Update a shared component once and have it propagate to all templates
- ✅ Preview and test all templates together
- ✅ Deploy email changes alongside app changes in the same PR
Option 1: npm Workspaces Setup
Create the project structure
mkdir email-templates-monorepo
cd email-templates-monorepo
npm init -y
Enable npm workspaces inside package.json:
{ "name": "email-templates-monorepo", "private": true, "workspaces": ["packages/*"]}
Create Your Email Package
mkdir -p packages/transactional
cd packages/transactional
npm init -y
Install React Email dependencies:
npm install @react-email/components react react-dom
npm install --save-dev typescript @types/react @types/react-dom
Option 2: pnpm Workspaces Setup
If you use pnpm (recommended for monorepos due to faster installs and disk efficiency):
mkdir email-templates-monorepo
cd email-templates-monorepo
pnpm init
Create pnpm-workspace.yaml in the root:
packages: - 'packages/*'
Then create your email package:
mkdir -p packages/transactional
cd packages/transactional
pnpm init
pnpm add @react-email/components react react-dom
pnpm add -D typescript @types/react @types/react-dom
Recommended Folder Structure
Here is a structure that scales well for a SaaS product:
email-templates-monorepo/
├── packages/
│ ├── transactional/ # Triggered by user actions
│ │ ├── emails/
│ │ │ ├── welcome-email.tsx
│ │ │ ├── otp-email.tsx
│ │ │ ├── magic-link.tsx
│ │ │ ├── password-reset.tsx
│ │ │ ├── payment-receipt.tsx
│ │ │ ├── trial-started.tsx
│ │ │ ├── trial-ending.tsx
│ │ │ └── security-alert.tsx
│ │ └── package.json
│ ├── marketing/ # Newsletters, campaigns
│ │ ├── emails/
│ │ │ ├── newsletter.tsx
│ │ │ └── product-update.tsx
│ │ └── package.json
│ └── shared/ # Shared components
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ └── Button.tsx
│ └── package.json
└── package.json
TypeScript Configuration
Add a tsconfig.json to each package. For packages/transactional:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": "./emails" }, "include": ["emails/**/*"]}
And a root tsconfig.base.json to share settings across packages:
{ "compilerOptions": { "target": "ES2020", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "jsx": "react-jsx" }}
Writing Your First Email Template
Inside packages/transactional/emails/welcome-email.tsx:
import * as React from 'react';import { Body, Button, Container, Head, Heading, Html, Preview, Section, Text,} from '@react-email/components';
interface WelcomeEmailProps { username: string; dashboardUrl: string;}
export const WelcomeEmail = ({ username, dashboardUrl }: WelcomeEmailProps) => ( <Html> <Head /> <Preview>Welcome — your account is ready!</Preview> <Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f4f4f5' }}> <Container style={{ margin: '0 auto', padding: '40px 20px', maxWidth: '560px' }}> <Heading>Welcome, {username}!</Heading> <Text>Your account is set up and ready to use.</Text> <Section> <Button href={dashboardUrl} style={{ backgroundColor: '#000', color: '#fff', padding: '12px 24px', borderRadius: '6px' }}> Go to Dashboard </Button> </Section> </Container> </Body> </Html>);
WelcomeEmail.PreviewProps = { username: 'Alex', dashboardUrl: 'https://example.com/dashboard',} as WelcomeEmailProps;
export default WelcomeEmail;
Setting Up the Preview Server
Update packages/transactional/package.json:
{ "scripts": { "dev": "email dev --dir emails", "export": "email export --dir emails --out dist" }}
Start the preview server from the monorepo root:
npm run dev -w packages/transactional
# or with pnpm:
pnpm --filter transactional dev
Open http://localhost:3000 to preview all your email templates in the browser.
Sharing Components Between Templates
Create a shared Header component in packages/shared/components/Header.tsx:
import * as React from 'react';import { Img, Section } from '@react-email/components';
interface HeaderProps { logoUrl: string; logoAlt?: string;}
export const Header = ({ logoUrl, logoAlt = 'Logo' }: HeaderProps) => ( <Section style={{ backgroundColor: '#000', padding: '20px 40px' }}> <Img src={logoUrl} alt={logoAlt} width={120} /> </Section>);
Add a package.json for the shared package:
{ "name": "@myapp/email-shared", "version": "1.0.0", "main": "./components/index.ts", "types": "./components/index.ts"}
Create packages/shared/components/index.ts to export everything:
export { Header } from './Header';export { Footer } from './Footer';export { Button } from './Button';
Then in packages/transactional/package.json, add the dependency:
{ "dependencies": { "@myapp/email-shared": "workspace:*" }}
Install with npm install or pnpm install from the root to link the workspace packages.
Now import shared components in any template:
import { Header, Footer } from '@myapp/email-shared';
Using with Turborepo (Optional)
If you already use Turborepo, add your email package to the pipeline in turbo.json:
{ "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true }, "export": { "dependsOn": ["^build"], "outputs": ["dist/**"] } }}
Run all email exports in parallel with the rest of your build:
turbo run export
Testing Your Email Templates
With React Email, there are two levels of testing:
1. Visual testing (always do this)
Run npm run dev in the transactional package and check each template at http://localhost:3000. Preview in desktop and mobile views.
2. Snapshot testing with Vitest
Add Vitest to your transactional package:
npm install --save-dev vitest @vitejs/plugin-react
Write a simple snapshot test:
// emails/__tests__/welcome-email.test.tsximport { render } from '@react-email/render';import { describe, expect, it } from 'vitest';import { WelcomeEmail } from '../welcome-email';
describe('WelcomeEmail', () => { it('renders without errors', async () => { const html = await render( <WelcomeEmail username="Alex" dashboardUrl="https://example.com" /> ); expect(html).toContain('Alex'); expect(html).toContain('https://example.com'); });});
Best Practices
- Keep shared components in a dedicated package — don't duplicate headers and footers across templates
- Use TypeScript everywhere — typed props prevent sending emails with missing data
- Use
PreviewPropson every template — makes local previewing instant and realistic - One email per file — easier to find, test, and update
- Consistent naming —
welcome-email.tsx,otp-email.tsx,payment-receipt.tsx - Test across clients before shipping — Gmail, Outlook, Apple Mail, mobile all render differently
What's Next?
Now that your monorepo is set up:
- Send Emails: Learn how to send React Email templates with Resend in production
- Get Inspiration: Browse React email template examples you can adapt
- Full SaaS email guide: Read about all the email templates a SaaS app needs
- Save Time: Get 12 production-ready templates already organized, typed, and tested — drop them straight into your monorepo
