Epic Stack 邮件服务接入指南:基于 Resend 的邮件发送配置、本地 Mock 与生产部署
2026/9/17 20:44:02 网站建设 项目流程

Epic Stack 邮件服务接入指南:基于 Resend 的邮件发送配置、本地 Mock 与生产部署

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

本篇指南完整讲解 Epic Stack 全栈启动模板中邮件模块的配置与使用:以 Resend 作为默认邮件服务商,覆盖 API Key 配置、from发件地址调整、@react-email邮件模板渲染、本地开发终端日志与 MSW Mock,以及 Playwright 端到端测试中对邮件内容的断言。读完本文,你将能够在自己的 Epic Stack 应用上快速启用真实邮件发送,并理解其"未配置也能运行、配置后立即可用"的渐进式接入设计。

一、为什么 Epic Stack 选择 Resend

Epic Stack 的邮件发送能力经历了两次关键决策,相关记录保留在仓库的 docs/decisions 目录中:

  • 002-email-service.md(已被取代):最初选择 Mailgun,理由是"免费额度慷慨且在生产环境得到验证",同时确立了"未配置环境变量也能部署,邮件仅打印到控制台"的渐进式接入原则。
  • 017-resend-email.md(当前生效):因 Mailgun 调整定价模型导致免费额度不透明,Epic Stack 迁移到 Resend。Resend 提供每月 3000 封的免费额度,UI 简洁易用,且价格更低。

017 号决策还明确了实现约束:不使用 Resend SDK,而是直接调用其 REST API,目的是避免与特定服务商过度耦合,方便日后切换其他邮件提供商。这一决策直接决定了 app/utils/email.server.ts 中sendEmail的实现形态——整个邮件模块只依赖一个fetch调用和两个 zod schema。

二、邮件发送的核心实现:sendEmail 与 React Email

Epic Stack 的邮件发送统一收敛在 app/utils/email.server.ts 的sendEmail函数中,所有业务路由都通过它发信,绝不散落各处。

2.1 函数签名与双模式入参

export async function sendEmail({ react, ...options }: { to: string subject: string } & ( | { html: string; text: string; react?: never } | { react: ReactElement; html?: never; text?: never } ))

调用方可以二选一:

  • 传入htmltext两个字符串,直接作为邮件正文;
  • 传入一个react元素(ReactElement),由@react-email/components渲染为 HTML 与纯文本两种版本。

第二种方式是 Epic Stack 的推荐用法。以注册邮件为例,app/routes/_auth/signup.tsx 中定义了SignupEmail组件:

export function SignupEmail({ onboardingUrl, otp, }: { onboardingUrl: string otp: string }) { return ( <E.Html lang="en" dir="ltr"> <E.Container> <h1> <E.Text>Welcome to Epic Notes!</E.Text> </h1> <p> <E.Text> Here's your verification code: <strong>{otp}</strong> </E.Text> </p> <p> <E.Text>Or click the link to get started:</E.Text> </p> <E.Link href={onboardingUrl}>{onboardingUrl}</E.Link> </E.Container> </E.Html> ) }

邮件内容同时包含一次性验证码(OTP)验证链接两条通路,用户任选其一即可完成验证。发送时通过renderReactEmail并行渲染 HTML 与纯文本:

async function renderReactEmail(react: ReactElement) { const [html, text] = await Promise.all([ render(react), render(react, { plainText: true }), ]) return { html, text } }

2.2 未配置 API Key 时的降级行为

sendEmail在发送前检查环境变量,这是"可选接入"设计的关键:

// feel free to remove this condition once you've set up resend if (!process.env.RESEND_API_KEY && !process.env.MOCKS) { console.error(`RESEND_API_KEY not set and we're not in mocks mode.`) console.error( `To send emails, set the RESEND_API_KEY environment variable.`, ) console.error(`Would have sent the following email:`, JSON.stringify(email)) return { status: 'success', data: { id: 'mocked' }, } as const }
  • 本地开发(未设置MOCKS)时:邮件不会真的发出,而是把完整邮件对象打印到终端,供开发者阅读。
  • 测试环境(设置了MOCKS)时:请求交给 MSW Mock 处理(详见第四节)。
  • 生产环境:如果忘记设置RESEND_API_KEY,会打印警告日志并模拟成功返回,应用不会因此崩溃。

2.3 直连 Resend REST API

配置就绪后,sendEmail直接 POST 到 Resend 的邮件发送端点,并使用 Bearer Token 鉴权:

const response = await fetch('https://api.resend.com/emails', { method: 'POST', body: JSON.stringify(email), headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, })

响应使用两个 zod schema 解析,保证类型安全:

  • resendSuccessSchema{ id: string },成功时返回该 id;
  • resendErrorSchema:包含namemessagestatusCode,并兜底一个UnknownError(500)分支,将无法解析的响应原样存入cause

函数最终返回带status: 'success' | 'error'的判别联合类型,调用方据此决定重定向还是回显错误。

2.4 环境变量声明

RESEND_API_KEY在 app/utils/env.server.ts 中声明为可选变量:

// If you plan to use Resend, remove the .optional() RESEND_API_KEY: z.string().optional(),

注释明确提示:一旦你决定正式启用 Resend,就应移除.optional(),让应用在启动时校验该变量必须存在。init()会在启动阶段用schema.safeParse(process.env)校验全部环境变量,缺失必填项时打印❌ Invalid environment variables并抛错退出。

三、接入 Resend 的完整操作步骤

以下步骤与 docs/email.md 完全一致,并补充了源码层面的说明。

第 1 步:创建 API Key

前往 Resend 控制台创建 API Key(形如re_blAh_blaHBlaHblahBLAhBlAh)。

第 2 步:为生产与 Staging 环境设置密钥

Epic Stack 默认使用 Fly.io 部署(参考 fly.toml),因此通过fly secrets set写入:

fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME] fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME]-staging

两条命令分别针对正式应用与-staging后缀的预发布应用。Fly Secrets 会作为环境变量注入,sendEmail运行时即可读到。

第 3 步:配置自定义发送域名

在 Resend 控制台的 Domains 页面添加并验证你的自定义发送域名(建议与主域名一致,有助于送达率)。验证方式通常是添加 DNS 记录(SPF、DKIM 等),具体以 Resend 后台指引为准。

第 4 步:修改 from 发件地址

自定义域名验证通过后,将 app/utils/email.server.ts 第 34 行的默认发件人改为你的域名地址:

const from = 'hello@epicstack.dev'

例如改为noreply@yourdomain.com。注意该from地址必须与你配置的发送域名匹配,否则 Resend 会拒绝发送。

第 5 步:同步更新端到端测试断言

仓库中的 tests/e2e/onboarding.test.ts 在多个测试里断言了发件人地址,例如:

expect(email.to).toBe(onboardingData.email.toLowerCase()) expect(email.from).toBe('hello@epicstack.dev') expect(email.subject).toMatch(/welcome/i)

以及重置密码测试中的expect(email.subject).toMatch(/password reset/i)。修改from后,必须把这些expect(email.from).toBe(...)断言一并更新为你新的发件人地址,否则 CI 中的 Playwright 测试会失败。

第 6 步:让 RESEND_API_KEY 变为必填

正式启用后,按 app/utils/env.server.ts 中的注释移除RESEND_API_KEY.optional(),使应用在缺失该变量时启动即失败,避免"静默降级"掩盖配置遗漏。

四、本地开发与测试:MSW Mock 与邮件 Fixtures

4.1 Mock 处理器

仓库用 MSW(Mock Service Worker)拦截真实邮件请求。在 tests/mocks/resend.ts 中:

export const handlers: Array<HttpHandler> = [ http.post(`https://api.resend.com/emails`, async ({ request }) => { requireHeader(request.headers, 'Authorization') const body = await request.json() console.info('🔶 mocked email contents:', body) const email = await writeEmail(body) return json({ id: faker.string.uuid(), from: email.from, to: email.to, created_at: new Date().toISOString(), }) }), ]

Mock 会校验请求必须携带Authorization头(对应真实场景中的 Bearer Token),把邮件写入 fixtures,并在终端打印🔶 mocked email contents:,最后返回一个与真实 Resend 响应结构一致的对象。

4.2 邮件 Fixtures 的读写工具

tests/mocks/utils.ts 提供了一套完整的邮件存取工具:

  • EmailSchema:zod 校验tofromsubjecttexthtml五个字段;
  • writeEmail(rawEmail):解析并写入tests/fixtures/email/<收件人>.json
  • readEmail(recipient)/requireEmail(recipient):按收件人读取邮件,供测试断言使用。

开发与测试期间,邮件以 JSON 文件形式落在 fixtures 目录,e2e 测试可以通过readEmail拿到完整邮件对象进行断言——这就是 tests/e2e/onboarding.test.ts 能校验email.fromemail.subject、甚至从email.text中正则提取验证链接与验证码(CODE_REGEX)的原因。

4.3 邮件在测试中的完整链路

以注册流程为例,测试会:

  1. 填写邮箱并提交注册表单;
  2. 调用readEmail(onboardingData.email)读取"发送"的邮件;
  3. 断言收件人、发件人、主题;
  4. 用正则/(?<url>https?:\/\/[^\s$.?#].[^\s]*)/从纯文本中提取验证 URL,或用CODE_REGEX提取验证码;
  5. 带着提取到的链接或验证码走完整个注册流程。

五、邮件在应用中的真实业务场景

从源码看,Epic Stack 的邮件目前用于三类身份验证场景,均通过prepareVerification(定义于 app/routes/_auth/verify.server.ts)生成 TOTP 验证码并写入数据库:

  1. 注册(onboarding):app/routes/_auth/signup.tsx 中prepareVerification({ period: 10 * 60, type: 'onboarding', target: email })生成 10 分钟有效的验证信息,随后调用sendEmail发送带验证码与验证链接的欢迎邮件。
  2. 找回密码(reset-password):app/routes/_auth/forgot-password.tsx 中通过用户名或邮箱定位用户,发送Epic Notes Password Reset主题的邮件。
  3. 修改邮箱(change-email):app/routes/settings/profile/change-email.server.tsx 与 change-email.tsx 同样借助邮件验证新邮箱的归属权。

所有场景共用同一个sendEmail,验证码由generateTOTP生成,字符集特意排除了易混淆的0OIABCDEFGHJKLMNPQRSTUVWXYZ123456789),提升人工输入准确率。这正是"发送逻辑收敛单点、业务场景各取所需"的架构收益。

六、常见问题与注意事项

  • 开发时看不到邮件?未设置MOCKS时邮件打印在终端(Would have sent the following email:),记得观察运行 dev server 的终端输出;设置了MOCKS的环境则看🔶 mocked email contents:日志并检查 tests/fixtures/email 目录。
  • from地址被 Resend 拒绝?检查发送域名是否已完成 DNS 验证,且from属于该域名。
  • 测试红了吗?修改from后务必同步更新 tests/e2e/onboarding.test.ts 中的expect(email.from).toBe(...)
  • 想要换一家邮件服务商?得益于 017 号决策的 REST API 设计,你只需替换 app/utils/email.server.ts 中的fetch端点和 tests/mocks/resend.ts 中的 Mock 处理器,业务路由代码无需改动。

七、小结

Epic Stack 的邮件能力围绕 docs/email.md 的指引即可快速落地:创建 Resend API Key →fly secrets set写入生产与 Staging → 配置发送域名 → 修改from并同步测试断言 → 移除环境变量.optional()。底层由 app/utils/email.server.ts 的sendEmail统一承载,配合@react-email模板渲染、MSW Mock 与 Fixtures 读写工具,形成了一套"开发期可读、测试期可断言、生产期可送达"的完整邮件解决方案。

【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询