Adicionar Email Transacional Com o Claude Code (Resend + React Email)
Como construir emails de boas-vindas, recibos e reposição de password numa app Next.js 16 usando o Resend e o React Email, com o Claude Code a escrever os templates e a lógica de envio.
Quer o framework por trás destes projetos?
Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.
Todo o SaaS precisa de três emails no primeiro dia: boas-vindas, recibo, reposição de password. Errá-los (aterrar no spam, faltar um registo DKIM, falhar em silêncio por causa de um erro de escrita) custa-te utilizadores antes mesmo de acabarem o onboarding. Este é um passo a passo completo para ligar o Resend e o React Email numa app Next.js 16 com o Claude Code, desde a verificação do domínio até a um envio a funcionar dentro de uma Server Action.
Porque É Que o Email Transacional É Chato
O email transacional parece simples até o pores mesmo em produção. Precisas de um domínio de envio verificado ou os teus emails vão para o spam, ou nem sequer são enviados. Precisas de HTML que renderize de forma consistente no Gmail, no Outlook e no Apple Mail, que têm suporte de CSS radicalmente diferente. Precisas de disparar o envio no momento certo (depois de o registo terminar, depois de um pagamento ter sucesso) sem bloquear a resposta que o utilizador está à espera. E precisas de saber quando um envio falhou de verdade em vez de assumir que correu bem.
O Resend trata da entrega e da reputação do domínio. O React Email trata dos templates, para que escrevas JSX em vez de layouts de tabela feitos à mão. O Claude Code é útil aqui porque já conhece bem ambas as APIs e consegue ligar a canalização (Server Actions, tratamento de erros, variáveis de ambiente) corretamente à primeira, desde que lhe digas exatamente o que construir.
Instalar o Resend e o React Email
Precisas de três pacotes: o SDK do Resend para enviar email, a biblioteca de componentes do React Email para construir templates, e a CLI do React Email para os pré-visualizar localmente. Pede ao Claude Code para instalar os três e ele usa o gestor de pacotes do teu projeto automaticamente.
npm install resend @react-email/components
npm install -D react-emailO resend é o cliente que fala com a API do Resend. O @react-email/components dá-te primitivas pré-construídas e seguras para email (Html, Body, Container, Button, Text) que renderizam corretamente nos vários clientes de email sem teres de lutar com CSS inline. O react-email é uma dependência só de desenvolvimento, é ela que alimenta o servidor de pré-visualização local que vais usar mais à frente.
Obter uma Chave de API e Verificar o Teu Domínio de Envio
Regista-te em resend.com e cria uma chave de API a partir do dashboard. Guarda-a como uma variável de ambiente, nunca a metas hardcoded num template nem faças commit dela.
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM="Your App <hello@yourdomain.com>"Sem um domínio verificado, o Resend só te deixa enviar para o email da tua própria conta usando o endereço partilhado onboarding@resend.dev. Isso serve para um primeiro teste, mas os utilizadores reais não vão receber nada a partir daí. Para enviar para caixas de entrada reais, vai a Domains no dashboard do Resend, adiciona o teu domínio, e adiciona os registos de SPF e DKIM que ele te dá ao teu fornecedor de DNS. A verificação costuma levar uns minutos assim que os registos se propagam, ocasionalmente mais dependendo do teu registrar.
Diz ao Claude Code que domínio estás a usar e ele vai lembrar-te de verificar o estado de verificação antes de testar um envio real:
claude "add a startup check that warns in the console if EMAIL_FROM's domain isn't verified in Resend, using the domains.list API"Construir um Template de React Email
Os templates vivem na sua própria pasta para que o servidor de pré-visualização os encontre e para que fiquem separados dos componentes de UI da tua app. Cada template é um componente React simples construído a partir das primitivas do @react-email/components, que mapeiam para HTML seguro para email nos bastidores.
// emails/welcome-email.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
interface WelcomeEmailProps {
name: string;
dashboardUrl: string;
}
export default function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Your account is ready</Preview>
<Body style={{ backgroundColor: "#f6f6f6", fontFamily: "sans-serif" }}>
<Container style={{ backgroundColor: "#ffffff", padding: "32px", borderRadius: "8px" }}>
<Heading style={{ fontSize: "20px" }}>Welcome, {name}</Heading>
<Text style={{ color: "#444", fontSize: "15px", lineHeight: "22px" }}>
Your account is set up and ready to go. Click below to get started.
</Text>
<Section style={{ marginTop: "24px" }}>
<Button
href={dashboardUrl}
style={{
backgroundColor: "#111",
color: "#fff",
padding: "12px 20px",
borderRadius: "6px",
fontSize: "14px",
}}
>
Go to dashboard
</Button>
</Section>
</Container>
</Body>
</Html>
);
}Os estilos inline aqui são intencionais, não um atalho. A maioria dos clientes de email retira os blocos <style> ou o CSS baseado em classes, por isso o @react-email/components renderiza os teus estilos inline no HTML final automaticamente. Escreves JSX de aspeto normal e sai seguro para email.
Um Auxiliar lib/email
Todas as rotas que enviam email precisam do mesmo cliente Resend, por isso ele pertence a um único auxiliar partilhado em vez de ser reinstanciado por todo o lado. Isto também te dá um único sítio para adicionar logging ou um fallback mais tarde.
// lib/email.ts
import { Resend } from "resend";
import type { ReactElement } from "react";
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailParams {
to: string;
subject: string;
react: ReactElement;
}
export async function sendEmail({ to, subject, react }: SendEmailParams) {
const { data, error } = await resend.emails.send({
from: process.env.EMAIL_FROM!,
to,
subject,
react,
});
if (error) {
console.error("Email send failed:", error);
return { success: false as const, error };
}
return { success: true as const, id: data?.id };
}O SDK do Resend não lança exceção num envio falhado, devolve um campo error no objeto de resposta. Se saltares essa verificação, um email devolvido ou rejeitado parece exatamente igual a um bem-sucedido nos teus logs. O auxiliar acima obriga cada quem chama a tratar ambos os casos de forma explícita.
Enviar a Partir de uma Server Action (Email de Boas-Vindas Depois do Registo)
O email de boas-vindas deve disparar logo a seguir à criação da conta, sem fazer o utilizador esperar pela resposta do fornecedor de email antes de aterrar na app. Envolve o envio na Server Action que corre depois de o registo terminar.
// app/(auth)/signup/actions.ts
"use server";
import { createUser } from "@/lib/db";
import { sendEmail } from "@/lib/email";
import WelcomeEmail from "@/emails/welcome-email";
import { redirect } from "next/navigation";
export async function signUp(formData: FormData) {
const email = formData.get("email") as string;
const name = formData.get("name") as string;
const password = formData.get("password") as string;
const user = await createUser({ email, name, password });
const result = await sendEmail({
to: user.email,
subject: "Welcome aboard",
react: WelcomeEmail({
name: user.name,
dashboardUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
}),
});
if (!result.success) {
console.error(`Welcome email failed for user ${user.id}`, result.error);
}
redirect("/dashboard");
}Um email de boas-vindas falhado nunca deve bloquear a criação da conta. O utilizador já tem uma conta nesse ponto, por isso o código regista a falha e continua para o dashboard em vez de lançar uma exceção. Aquela linha de log importa, pede ao Claude Code para a ligar ao teu rastreador de erros existente, se tiveres um, para que um pico de falhas de email seja mesmo notado em vez de ficar quietinho nos logs do servidor.
Enviar a Partir de um Route Handler (Reposição de Password)
A reposição de password precisa de um token gerado antes do envio, e é muitas vezes disparada a partir de um formulário que espera uma resposta JSON em vez de um redirect, por isso um Route Handler encaixa melhor aqui do que uma Server Action.
// app/api/auth/reset-password/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createResetToken, getUserByEmail } from "@/lib/db";
import { sendEmail } from "@/lib/email";
import ResetPasswordEmail from "@/emails/reset-password-email";
export async function POST(request: NextRequest) {
const { email } = await request.json();
const user = await getUserByEmail(email);
// Always return success, even if the user doesn't exist.
// This avoids leaking which emails have accounts.
if (!user) {
return NextResponse.json({ success: true });
}
const token = await createResetToken(user.id);
const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password/${token}`;
const result = await sendEmail({
to: user.email,
subject: "Reset your password",
react: ResetPasswordEmail({ resetUrl }),
});
if (!result.success) {
return NextResponse.json({ success: false, error: "Failed to send email" }, { status: 500 });
}
return NextResponse.json({ success: true });
}Devolver a mesma resposta de sucesso quer a conta exista ou não é um detalhe de segurança pequeno mas real. Pede ao Claude Code este padrão de forma explícita ("don't leak account existence through the response") porque uma implementação ingénua devolve um 404 para emails desconhecidos, que é exatamente a informação que não queres entregar.
Construir o Email de Recibo
Os recibos seguem o mesmo padrão do email de boas-vindas, apenas com conteúdo diferente e, normalmente, um webhook do Stripe como disparo em vez de uma submissão de formulário.
// emails/receipt-email.tsx
import { Body, Container, Head, Heading, Hr, Html, Row, Column, Section, Text } from "@react-email/components";
interface ReceiptEmailProps {
amount: string;
planName: string;
invoiceDate: string;
}
export default function ReceiptEmail({ amount, planName, invoiceDate }: ReceiptEmailProps) {
return (
<Html>
<Head />
<Body style={{ backgroundColor: "#f6f6f6", fontFamily: "sans-serif" }}>
<Container style={{ backgroundColor: "#ffffff", padding: "32px", borderRadius: "8px" }}>
<Heading style={{ fontSize: "20px" }}>Payment received</Heading>
<Section style={{ marginTop: "16px" }}>
<Row>
<Column><Text>Plan</Text></Column>
<Column><Text>{planName}</Text></Column>
</Row>
<Row>
<Column><Text>Amount</Text></Column>
<Column><Text>{amount}</Text></Column>
</Row>
<Row>
<Column><Text>Date</Text></Column>
<Column><Text>{invoiceDate}</Text></Column>
</Row>
</Section>
<Hr />
<Text style={{ color: "#888", fontSize: "12px" }}>
Questions about this charge? Reply to this email.
</Text>
</Container>
</Body>
</Html>
);
}Chama isto a partir do teu handler de webhook do Stripe depois de um evento checkout.session.completed ou invoice.paid, usando o mesmo auxiliar sendEmail de antes. O Stripe repete os webhooks falhados, por isso se o teu envio de email lançar uma exceção dentro desse handler, envolve-o para que um envio mau não faça o Stripe pensar que o webhook inteiro falhou e repita o evento todo.
Pré-visualizar os Templates Localmente
Não queres enviar um email real cada vez que ajustas o tamanho de uma fonte. A CLI do React Email arranca um servidor local que renderiza os teus templates ao vivo e recarrega ao guardar, sem chave de API nem chamada de rede necessárias.
npx react-email devIsto abre uma pré-visualização em localhost:3000 (ou a próxima porta livre) a mostrar todos os componentes na tua pasta emails/, renderizados como apareceriam numa caixa de entrada, com um toggle para inspecionar o output HTML puro. Aponta-o para uma pasta personalizada com --dir se os teus templates não viverem na localização predefinida. Pede ao Claude Code para passar props de teste para cada template para que a pré-visualização mostre conteúdo de aspeto real em vez de campos vazios.
Tratar Erros e Idempotência
Dois modos de falha importam em produção. Primeiro, uma chamada lenta ou falhada ao Resend dentro de uma Server Action pode deixar um pedido pendurado se não definires uma expectativa razoável de quanto tempo vais esperar. Segundo, as repetições (um utilizador a fazer duplo clique no submit, um webhook a disparar duas vezes) podem provocar envios duplicados se nada as parar.
Para proteção contra duplicados, passa uma chave de idempotência nos envios disparados por eventos externos como webhooks, para que um webhook repetido não envie o mesmo recibo duas vezes.
await resend.emails.send(
{
from: process.env.EMAIL_FROM!,
to: user.email,
subject: "Payment received",
react: ReceiptEmail({ amount, planName, invoiceDate }),
},
{ idempotencyKey: `receipt-${invoiceId}` }
);Usar o ID da fatura do Stripe como chave de idempotência significa que o Resend reconhece um pedido repetido com a mesma chave e salta o envio, em vez de disparar um segundo recibo. Isto importa mais nos envios disparados por webhook, onde as repetições são comportamento esperado, não um caso raro.
Testar Antes de Pores em Produção
Antes de ligares isto ao registo ou ao checkout, envia um email de teste real para a tua própria caixa de entrada e verifica três coisas à mão: a linha de assunto não está truncada, o botão liga mesmo ao URL certo, e o email não aterra no spam. O Gmail e o Outlook tratam os estilos inline de forma diferente, por isso verifica ambos se conseguires.
claude "send a test welcome email to my inbox using the sendEmail helper and confirm the response includes a data.id"Assim que isso funcionar, verifica o separador Logs do dashboard do Resend. Cada envio mostra o seu estado de entrega (delivered, bounced, complained), que é a forma mais rápida de confirmar que a verificação do teu domínio está mesmo a funcionar de ponta a ponta, não apenas aceite pela API.
Ligar o email à mão, um endpoint de cada vez, é o tipo de tarefa que o Claude Code trata bem numa única sessão. A parte mais difícil é tudo à volta: verificar o domínio corretamente, garantir que um envio falhado não parte o registo em silêncio, e manter os templates consistentes à medida que adicionas mais. O Code Kit de $29 entrega isto já ligado à framework que ele configura, templates de boas-vindas, recibo e reposição incluídos, para que comeces a partir de um sistema de email a funcionar em vez de construíres um do zero.
Posted by @speedy_devv
Quer o framework por trás destes projetos?
Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.
Adicionar Autenticação Com o Claude Code (Supabase Auth)
Adiciona registo por email/password, Google OAuth, magic links, rotas protegidas e gestão de sessões a uma app Next.js 16 usando o Claude Code e o Supabase Auth.
Construir Uma API Type-Safe Com o Claude Code (oRPC + Zod)
Como construir uma camada de API totalmente type-safe em Next.js 16 com oRPC e Zod, usando o Claude Code, para que as mudanças de esquema partam a build em vez da produção.

