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

📅 发布时间:2026/9/17 20:44:04
Epic Stack 邮件服务接入指南:基于 Resend 的邮件发送配置、本地 Mock 与生产部署
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 选择 ResendEpic 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 EmailEpic 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 } ))调用方可以二选一传入html与text两个字符串直接作为邮件正文传入一个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 langen dirltr E.Container h1 E.TextWelcome to Epic Notes!/E.Text /h1 p E.Text Heres your verification code: strong{otp}/strong /E.Text /p p E.TextOr 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 youve set up resend if (!process.env.RESEND_API_KEY !process.env.MOCKS) { console.error(RESEND_API_KEY not set and were 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 }成功时返回该 idresendErrorSchema包含name、message、statusCode并兜底一个UnknownError500分支将无法解析的响应原样存入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_KEYre_blAh_blaHBlaHblahBLAhBlAh --app [YOUR_APP_NAME] fly secrets set RESEND_API_KEYre_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 helloepicstack.dev例如改为noreplyyourdomain.com。注意该from地址必须与你配置的发送域名匹配否则 Resend 会拒绝发送。第 5 步同步更新端到端测试断言仓库中的 tests/e2e/onboarding.test.ts 在多个测试里断言了发件人地址例如expect(email.to).toBe(onboardingData.email.toLowerCase()) expect(email.from).toBe(helloepicstack.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 与邮件 Fixtures4.1 Mock 处理器仓库用 MSWMock Service Worker拦截真实邮件请求。在 tests/mocks/resend.ts 中export const handlers: ArrayHttpHandler [ 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 提供了一套完整的邮件存取工具EmailSchemazod 校验to、from、subject、text、html五个字段writeEmail(rawEmail)解析并写入tests/fixtures/email/收件人.jsonreadEmail(recipient)/requireEmail(recipient)按收件人读取邮件供测试断言使用。开发与测试期间邮件以 JSON 文件形式落在 fixtures 目录e2e 测试可以通过readEmail拿到完整邮件对象进行断言——这就是 tests/e2e/onboarding.test.ts 能校验email.from、email.subject、甚至从email.text中正则提取验证链接与验证码CODE_REGEX的原因。4.3 邮件在测试中的完整链路以注册流程为例测试会填写邮箱并提交注册表单调用readEmail(onboardingData.email)读取发送的邮件断言收件人、发件人、主题用正则/(?urlhttps?:\/\/[^\s$.?#].[^\s]*)/从纯文本中提取验证 URL或用CODE_REGEX提取验证码带着提取到的链接或验证码走完整个注册流程。五、邮件在应用中的真实业务场景从源码看Epic Stack 的邮件目前用于三类身份验证场景均通过prepareVerification定义于 app/routes/_auth/verify.server.ts生成 TOTP 验证码并写入数据库注册onboardingapp/routes/_auth/signup.tsx 中prepareVerification({ period: 10 * 60, type: onboarding, target: email })生成 10 分钟有效的验证信息随后调用sendEmail发送带验证码与验证链接的欢迎邮件。找回密码reset-passwordapp/routes/_auth/forgot-password.tsx 中通过用户名或邮箱定位用户发送Epic Notes Password Reset主题的邮件。修改邮箱change-emailapp/routes/settings/profile/change-email.server.tsx 与 change-email.tsx 同样借助邮件验证新邮箱的归属权。所有场景共用同一个sendEmail验证码由generateTOTP生成字符集特意排除了易混淆的0、O、IABCDEFGHJKLMNPQRSTUVWXYZ123456789提升人工输入准确率。这正是发送逻辑收敛单点、业务场景各取所需的架构收益。六、常见问题与注意事项开发时看不到邮件未设置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),仅供参考