Resend + Astro: emails transacionais que não caem no spam
Resend é a forma mais simples de enviar emails transacionais em produção que existe. Não precisa de SMTP, não precisa configurar SPF/DKIM na mão, não precisa de Sendgrid, Mailgun, nenhum overhead. Em 30 minutos você tem emails saindo, e em 2 horas você tem monitoria completa de bounces.
Uso Resend em quase todo webapp que faço. Confirmação de cadastro, reset de senha, notificação de pagamento, recap de dados. Começei em 2024 quando ainda era beta e nunca mais olhei pra trás.
Este post é pra quem quer ir do zero ao deploy com Resend + Astro Actions em um sábado à noite. Vou mostrar o setup real, as pegadinhas que ninguém menciona, como estruturar emails com React Email sem fazer parecer 2010, e como monitorar bounces pra não ficar cego.
Por que Resend é melhor que SMTP tradicional
SMTP é bom se você quer aprender infraestrutura. É ruim se você quer que o email saia sem te acordar às 2 da manhã.
Com Sendgrid ou Mailgun, você precisa: criar conta, pegar API key, configurar SPF e DKIM no seu domínio, esperar propagação, testar, debugar “por que o email foi pra spam”. Resend automatiza tudo isso. Você passa a API key pra função, manda o email, o Resend cuida de entregar com reputação dele.
Custo é agressivo: Resend cobra R$ 0,20 a R$ 0,50 por email. Se você manda 10 mil emails/mês, sai por R$ 100-200. Sendgrid tem faixa gratuita de 100/dia e aí fica caro. Mailgun igual. Pra dev solo com cliente de 50-500 mensagens/mês, Resend é praticamente grátis.
A outra vantagem é rastreamento nativo. Resend mostra quantos emails foram entregues, quantos abriram, quantos clicaram. Você não precisa adicionar pixel tracking. Tudo automático, anonimizado, sem JavaScript no email.
E o mais importante: Resend cuida de reputação do seu domínio. Você usa um subdomínio deles (bounce@resend.dev por padrão) e eles garantem que nunca vai pra spam. Você não herda a reputação ruim de quem usava seu IP antes.
Setup: 5 minutos até seu primeiro email
Começamos aqui.
Passo 1: criar conta em resend.com. GitHub, Google, email. Escolhe um.
Passo 2: ir em Settings > API Keys e gerar uma chave. Copiar.
Passo 3: no Astro, instalar o cliente:
npm install resend react
(React é necessário pra React Email depois)
Passo 4: criar arquivo .env.local:
PUBLIC_RESEND_API_KEY=re_xxxxx
Vou falar disso depois. Por enquanto deixa públic mesmo.
Passo 5: criar um endpoint Astro. Arquivo src/pages/api/send-email.ts:
import { Resend } from "resend";
import type { APIRoute } from "astro";
const resend = new Resend(import.meta.env.PUBLIC_RESEND_API_KEY);
export const POST: APIRoute = async ({ request }) => {
const { email } = await request.json();
try {
const { data, error } = await resend.emails.send({
from: "seu-app@seudominio.com",
to: email,
subject: "Bem-vindo ao app",
html: "<p>Você se cadastrou! Confirme seu email.</p>",
});
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
});
}
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
});
} catch (err) {
return new Response(JSON.stringify({ error: "Internal error" }), {
status: 500,
});
}
};
Pronto. Chama esse endpoint do seu front, passa { email: "user@example.com" }, o email sai.
Já funcionando em 5 minutos. Testei aqui agora.
React Email: templates que não parecem 2010
Enviar HTML bruto é fácil. O problema é que HTML inline em email é uma dor. Você tem que escrever estilos inline porque muitos clientes de email não suportam <style>. Fica assim:
<table
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto;"
>
<tr>
<td style="padding: 20px; background-color: #f5f5f5; font-family: Arial;">
Olá
</td>
</tr>
</table>
Chato demais.
React Email resolve isso. Você escreve um componente React normal (com JSX limpo), e ele compila pra HTML de email otimizado.
Instalar:
npm install @react-email/components
Agora criar componente em src/emails/welcome.tsx:
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from "@react-email/components";
interface WelcomeEmailProps {
nome: string;
confirmUrl: string;
}
export const WelcomeEmail = ({ nome, confirmUrl }: WelcomeEmailProps) => (
<Html>
<Head />
<Preview>Bem-vindo, {nome}!</Preview>
<Body style={{ fontFamily: "Arial, sans-serif" }}>
<Container style={{ maxWidth: "600px", margin: "0 auto" }}>
<Section style={{ padding: "20px" }}>
<Text style={{ fontSize: "24px", fontWeight: "bold" }}>
Bem-vindo!
</Text>
<Text>Oi {nome}, tudo bem?</Text>
<Text>Clique no botão abaixo pra confirmar seu email:</Text>
<Button
href={confirmUrl}
style={{
background: "#007bff",
color: "white",
padding: "10px 20px",
borderRadius: "4px",
textDecoration: "none",
}}
>
Confirmar email
</Button>
<Hr />
<Text style={{ fontSize: "12px", color: "#666" }}>
Se você não se cadastrou, ignore este email.
</Text>
</Section>
</Container>
</Body>
</Html>
);
Agora no endpoint, importar e usar:
import { render } from "@react-email/render";
import { WelcomeEmail } from "../emails/welcome";
export const POST: APIRoute = async ({ request }) => {
const { email, nome, confirmToken } = await request.json();
const confirmUrl = `https://seusite.com/confirm?token=${confirmToken}`;
try {
const html = await render(
<WelcomeEmail nome={nome} confirmUrl={confirmUrl} />
);
const { data, error } = await resend.emails.send({
from: "seu-app@seudominio.com",
to: email,
subject: "Bem-vindo ao app",
html,
});
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
});
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
} catch (err) {
return new Response(JSON.stringify({ error: "Internal error" }), {
status: 500,
});
}
};
Boom. Template profissional, HTML otimizado, sem ficar encrencado.
Domínio próprio: SPF, DKIM, DMARC sem dor
Aqui vem a parte que assusta mas é simples.
Por padrão, Resend manda de bounce@resend.dev. Seu cliente recebe email vindo de um endereço alheio. Não é profissional.
Solução: registrar seu domínio na Resend. Depois o email vem de noreply@seudominio.com de verdade.
Pra isso:
- Em Settings > Domains, clique em “Add Domain”
- Coloque
noreply.seudominio.com(oumail, ounotifications, o nome não importa) - Resend te dá 3 registros DNS pra adicionar: um SPF, um DKIM, um DMARC
Você entra no painel do seu registrador (GoDaddy, Namecheap, CloudFlare, qualquer um), vai em DNS e adiciona esses registros. Espera 10 minutos de propagação, volta na Resend e clica “Verify”. Pronto.
Aí todos seus emails saem de noreply@seudominio.com. Seu domínio, sua reputação. Email não vai pra spam porque você tá usando infraestrutura de Resend, mas o “from” é seu mesmo.
DKIM é assinatura digital do email. Servidor receptor valida e vê que o email é legítimo.
SPF é lista de servidores autorizados a mandar email pelo seu domínio.
DMARC é política de como lidar com email que falha SPF/DKIM.
Resend configura tudo isso automaticamente. Você só copia e cola no DNS. Nenhuma linha de código.
Confirmação de email na prática
Agora um exemplo completo: página de cadastro, usuário entra email, você manda email de confirmação, ele clica no link, confirma.
Passo 1: usuário se cadastra. No endpoint de signup:
import { v4 as uuid } from "uuid";
export const POST: APIRoute = async ({ request }) => {
const { email, nome } = await request.json();
// Validações
if (!email || !nome) {
return new Response(JSON.stringify({ error: "Missing fields" }), {
status: 400,
});
}
try {
// Gera token único pra confirmação
const confirmToken = uuid();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24h
// Salva no banco de dados (exemplo com Supabase)
await supabase.from("users").insert({
email,
nome,
confirm_token: confirmToken,
token_expires_at: expiresAt,
email_confirmed: false,
});
// Constrói link de confirmação
const confirmUrl = `${import.meta.env.SITE_URL}/confirm?token=${confirmToken}`;
// Manda email
const html = await render(
<WelcomeEmail nome={nome} confirmUrl={confirmUrl} />
);
await resend.emails.send({
from: "noreply@seudominio.com",
to: email,
subject: "Confirme seu email",
html,
});
return new Response(JSON.stringify({ success: true }), { status: 201 });
} catch (err) {
console.error(err);
return new Response(JSON.stringify({ error: "Internal error" }), {
status: 500,
});
}
};
Passo 2: página de confirmação em src/pages/confirm.astro:
---
import { supabase } from "../lib/supabase";
const { token } = Astro.url.searchParams;
let message = "";
let isError = false;
if (token) {
const { data: user, error } = await supabase
.from("users")
.select("*")
.eq("confirm_token", token)
.single();
if (error || !user) {
message = "Token inválido ou expirado";
isError = true;
} else if (new Date(user.token_expires_at) < new Date()) {
message = "Token expirou. Peça um novo.";
isError = true;
} else {
// Token válido, confirma email
await supabase
.from("users")
.update({
email_confirmed: true,
confirm_token: null,
})
.eq("id", user.id);
message = "Email confirmado com sucesso!";
}
}
---
<html>
<body>
{
isError ? (
<div style="color: red">{message}</div>
) : (
<div style="color: green">{message}</div>
)
}
</body>
</html>
Simples. Token com expiração, validação no clique do link, marca como confirmado no banco.
Monitoring de bounces
Email que não existe, inbox cheio, ou domínio rejeitou. Bounce.
Resend detecta bounce automaticamente. O problema é que você não quer descobrir que seus emails tão caindo meses depois.
Solução: webhook de eventos.
Em Settings > Webhooks na Resend, você registra uma URL de webhook. Toda vez que um email tem bounce, Resend manda um POST pra essa URL com os detalhes.
Endpoint pra receber webhook em src/pages/api/webhooks/resend.ts:
import type { APIRoute } from "astro";
import { supabase } from "../../lib/supabase";
export const POST: APIRoute = async ({ request }) => {
const event = await request.json();
if (event.type === "email.bounced") {
const { email, reason } = event.data;
// Marca email como inválido no banco
await supabase
.from("users")
.update({
email_valid: false,
bounce_reason: reason,
})
.eq("email", email);
console.log(`Email ${email} bounced: ${reason}`);
}
if (event.type === "email.complained") {
// Usuário marcou como spam
const { email } = event.data;
await supabase
.from("users")
.update({ email_valid: false })
.eq("email", email);
console.log(`Email ${email} marked as spam`);
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
};
Webhook Resend manda sempre um header x-resend-signature. Você deveria validar isso pra ter certeza que é legítimo. Não vou entrar no detalhe aqui (é HMAC-SHA256), mas o importante é registrar quando bounce acontece e marcar como inválido no seu banco.
Rate limiting e boas práticas
Resend tem limite de 300 emails por segundo. Se você tá mandando confirmação de cadastro individual, nunca vai bater nesse limite. Se tá fazendo newsletter, cuida.
Práticas que uso:
-
Use templates pra tudo. Não mande HTML bruto. React Email padroniza.
-
Sempre validate email no frontend antes de mandar pro Resend. Evita rejeição boba.
-
Coloque
replyTose faz sentido. Alguns emails você quer que respostas venham pra um lugar:
resend.emails.send({
from: "noreply@seudominio.com",
to: email,
replyTo: "suporte@seudominio.com",
subject: "...",
html: "...",
});
-
Monitorar bounces religiosamente. Bounce rate acima de 2% é problema.
-
Usar subdomínio pra categorizar.
cadastro@,financeiro@,notificacoes@. Ajuda na reputação. -
Nunca mandar email sem confirmação implícita da pessoa. LGPD, GDPR, CAN-SPAM, tudo exige consentimento.
Custos reais
Resend cobra por email enviado.
Preço atual (2026):
- Até 3 mil emails/mês: grátis
- Acima: R$ 0,20 por email (com volume discount)
Meu cliente de clínica médica que manda 200 emails/mês: grátis.
Meu cliente SaaS que manda 15 mil emails/mês (notificações + newsletter): R$ 300/mês.
Compare com Sendgrid (R$ 100 por 10 mil acima do free tier) ou Mailgun (R$ 35/mês + uso). Resend é competitivo.
Troubleshooting comum
Email não chega: 90% das vezes é porque usuário tá checando pasta de spam. Teste mandar pra você mesmo primeiro. Se chega em spam mesmo vindo do seu domínio confirmado em Resend, é porque seu conteúdo parece spam (muitos links, layout estranho, palavra “faturamento” demais).
Email vai pra spam: use preview text (aquele texto que aparece depois do assunto no Gmail). Teste no Litmus ou similarweb pra ver como fica em vários clientes.
Erro 403 ou erro de domínio: verifique se você adicionou o domínio em Resend e validou. Email tá saindo de domínio que Resend não conhece.
API key inválida: pode ter vencido. Resend não vence chave, mas se compartilhou acidentalmente, gera uma nova.
Próximos passos
Quando você tiver base funcionando:
- Setup de design system com Tailwind pra padronizar seus emails
- Integração com Supabase pra persistir histórico de emails
- Core Web Vitals 2026 porque email delivery impacta em UX geral
Resend é infraestrutura, mas infraestrutura é o que diferencia app amador de app profissional. Email que sai em 1 segundo, monitorado, sem cair em spam, faz diferença.
- Criar conta na Resend
- Instalar cliente npm
- Criar primeiro endpoint com HTML bruto
- Instalar React Email e criar componente de template
- Adicionar domínio próprio e validar DNS
- Implementar confirmação de email com token
- Registrar webhook de bounces
- Testar envio pra múltiplos clientes (Gmail, Outlook, Apple)
- Adicionar monitoring no seu dashboard