Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Transactional email in Next.js App Router with Resend and react-email

May 29, 2026
nextjsresendreact-emailsaas

Transactional email is one of those features every SaaS needs and almost every tutorial oversimplifies. This post walks through the full pattern used in this Next.js App Router SaaS boilerplate: Resend as the delivery API, react-email for type-safe templates, and a service-layer architecture that keeps email sends out of routes and components.

Why Resend

Resend is a developer-first email API with a clean REST interface and a free tier that covers most early-stage products. The key advantage for a full-stack TypeScript project: the SDK is typed end-to-end and pairs directly with react-email, so your templates are React components -- previewable in the browser, refactorable with your IDE, and testable like any other function.

Install and configure

Add the packages:

npm install resend @react-email/components react-email

Add the API key to .env:

RESEND_API_KEY=re_your_key_here
EMAIL_FROM=onboarding@resend.dev

During development you can send from onboarding@resend.dev without domain verification. Production requires a verified sending domain added in the Resend dashboard.

The email service

Create lib/email.ts as the single send entry point for the whole app:

import { Resend } from 'resend';
import type { ReactElement } from 'react';
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
export const ENABLE_WELCOME_EMAIL =
  process.env.ENABLE_WELCOME_EMAIL === 'true';
export const ENABLE_EMAIL_VERIFICATION =
  process.env.ENABLE_EMAIL_VERIFICATION === 'true';
 
interface SendEmailOptions {
  to: string;
  subject: string;
  react: ReactElement;
}
 
export const emailService = {
  async sendEmail({ to, subject, react }: SendEmailOptions) {
    const { error } = await resend.emails.send({
      from: process.env.EMAIL_FROM ?? 'onboarding@resend.dev',
      to,
      subject,
      react,
    });
    if (error) throw new Error(`Email send failed: ${error.message}`);
  },
};

Two rules enforced here:

  • Call emailService only from the service layer, never from routes or components.
  • Service files end in .ts, not .tsx, so use React.createElement instead of JSX when constructing template elements.

Writing a template

Templates live in emails/. Each one is a plain React component using @react-email/components primitives:

// emails/WelcomeEmail.tsx
import {
  Html,
  Body,
  Heading,
  Text,
  Link,
  Preview,
} from '@react-email/components';
 
interface WelcomeEmailProps {
  appName: string;
  loginUrl: string;
}
 
export function WelcomeEmail({ appName, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Preview>Welcome to {appName}</Preview>
      <Body style={{ fontFamily: 'sans-serif', padding: '24px' }}>
        <Heading>Welcome to {appName}</Heading>
        <Text>
          Your account is ready. Click the link below to sign in.
        </Text>
        <Link href={loginUrl}>Sign in</Link>
      </Body>
    </Html>
  );
}

Use @react-email/components primitives instead of raw HTML tags. Email clients like Outlook and Gmail apply their own styles to bare <div> elements -- the components handle cross-client compatibility so you do not have to.

Sending from the service layer

Here is how userService sends a welcome email after registration:

// modules/user/user.service.ts
import React from 'react';
import { emailService, ENABLE_WELCOME_EMAIL } from '@/lib/email';
import { WelcomeEmail } from '@/emails/WelcomeEmail';
 
export const userService = {
  async register(data: RegisterInput) {
    // ... hash password, insert into DB via userRepo
 
    if (ENABLE_WELCOME_EMAIL) {
      await emailService.sendEmail({
        to: data.email,
        subject: `Welcome to ${process.env.NEXT_PUBLIC_APP_NAME}`,
        react: React.createElement(WelcomeEmail, {
          appName: process.env.NEXT_PUBLIC_APP_NAME ?? 'App',
          loginUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/login`,
        }),
      });
    }
  },
};

The ENABLE_WELCOME_EMAIL flag defaults to false. You will not accidentally send emails while iterating locally. Flip it to true in production environment variables when you are ready.

Previewing templates locally

react-email ships a preview server:

npx react-email dev --dir emails

This starts a UI at localhost:3000 that renders every template in emails/ with live reload. Check fonts, spacing, and link targets before sending a single real email.

Adding a password reset flow

Password reset needs a token column in the database. Add it to the user schema:

// modules/user/user.schema.ts
resetToken: text('reset_token'),
resetTokenExpiresAt: timestamp('reset_token_expires_at'),

Run the migration:

npm run db:generate
npm run db:migrate

The service generates a token, persists it, and sends the email:

async requestPasswordReset(email: string) {
  const user = await userRepo.findByEmail(email);
  if (!user) return; // silent -- do not leak whether the account exists
 
  const token = crypto.randomUUID();
  const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
 
  await userRepo.setResetToken(user.id, token, expiresAt);
 
  await emailService.sendEmail({
    to: email,
    subject: 'Reset your password',
    react: React.createElement(ResetPasswordEmail, {
      appName: process.env.NEXT_PUBLIC_APP_NAME ?? 'App',
      resetUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/reset-password?token=${token}`,
    }),
  });
},

The API route stays thin -- it calls userService.requestPasswordReset(email) and returns 200 regardless of whether the address exists. This prevents user enumeration.

Production checklist

Before enabling email in production:

  • Add and verify your sending domain in the Resend dashboard
  • Update EMAIL_FROM to a verified address (no-reply@yourdomain.com)
  • Set ENABLE_WELCOME_EMAIL=true in Vercel environment variables
  • Add RESEND_API_KEY via vercel env add RESEND_API_KEY
  • Re-run migrations if you added reset token columns

Takeaway

The pattern is: one emailService in lib/email.ts, templates as React components in emails/, all sends from the service layer behind feature flags, React.createElement instead of JSX in .ts files. Once this is wired up, adding new email triggers -- billing receipts, org invitations, verification flows -- is one new template file and one new emailService.sendEmail call. No new infrastructure, no new dependencies.