Servicio de Email (Resend)
Integra email transaccional con Resend. Verifica tu dominio, crea claves API, renderiza plantillas de bienvenida y pago en el servidor y envíalas a través de Resend con configuración por entorno.
Objetivo
Enviar emails automáticamente cuando ocurre algo en tu app, por ejemplo:
- Un usuario completa un pago con Stripe
- Un usuario se registra (email de bienvenida)
Usaremos Resend para entregar el email. Tu app generará el contenido (plantilla) y llamará a la API de Resend para enviarlo.
Qué es Resend y dónde encaja
- Tu app detecta un evento (webhook de Stripe o creación de usuario)
- Tu app renderiza una plantilla de email (HTML + texto opcional)
- Tu app llama a la API de Resend con los datos del email
- Resend lo entrega al destinatario y ofrece logs/eventos de entrega
Resend es el proveedor de envío y entregabilidad. No es una cola ni un worker. Puedes llamarlo directamente desde una ruta de servidor de Next.js (bien para empezar) o pasar el envío a un job/cola para mayor fiabilidad más adelante.
Configuración (una vez)
- Crea una cuenta en Resend y verifica tu dominio
- Regístrate en https://resend.com
- Añade tu dominio de envío (p. ej.,
tu-dominio.com
) - Añade los registros DNS que te indiquen (DKIM, SPF, y considera DMARC). Esto es necesario para buena entregabilidad.
- Crea una API key
- En el panel de Resend → API Keys → crea una clave secreta
- Guárdala privada; nunca la expongas en el cliente
- Añade variables de entorno
Agrega esto a .env.local
(y refleja en .env.example
después):
EMAIL_PROVIDER=resend
RESEND_API_KEY=tu_api_key_de_resend
EMAIL_FROM="Tu Nombre <founder@tu-dominio.com>"
Notas:
EMAIL_FROM
debe usar un dominio verificado en Resend- Mantén los secretos fuera del control de versiones; usa
.env.local
Atención:
- DNS: Usa preferentemente un subdominio como
send.tu-dominio.com
. En tu DNS, pon el host/servidor comosend
al crear los registros DKIM/SPF/CNAME que muestra Resend, y copia/pega los valores exactamente. - Remitente amigable: La primera parte de
EMAIL_FROM
es el nombre que verá el usuario en su bandeja. Usa un nombre real, por ejemplo"Ana de ToldYou <founder@toldyou.app>"
. Evitano-reply@…
; mejorfounder@
ohello@
.
- Instala el helper para renderizar plantillas
pnpm add @react-email/render
@react-email/render
convierte un componente React en HTML compatible con clientes de correo.
Estructura del proyecto
Separamos la lógica de email para poder cambiar de proveedor o plantillas fácilmente:
src/services/email/send.ts
— función única que envía con Resendsrc/services/email/templates/
— componentes React pequeños (bienvenida, etc.)
Preparar una plantilla (React Email)
Ejemplo de plantilla de bienvenida en src/services/email/templates/welcome.tsx
:
import * as React from "react";
export default function WelcomeEmail({ name }: { name?: string }) {
return (
<div style={{ fontFamily: 'Arial, sans-serif', lineHeight: 1.5 }}>
<h1>Welcome{ name ? `, ${name}` : '' }!</h1>
<p>Thanks for signing up — we’re excited to have you.</p>
<p>Questions? Just reply to this email.</p>
</div>
);
}
Es un componente React normal; lo renderizamos a HTML antes de enviarlo.
Función de envío (Resend)
Añadimos un helper en src/services/email/send.ts
:
import { Resend } from "resend";
import { render } from "@react-email/render";
type MailInput = { to: string; subject: string; html: string; text?: string; from?: string };
const resend = new Resend(process.env.RESEND_API_KEY!);
export async function sendMail({ to, subject, html, text, from }: MailInput) {
const fromEmail = from ?? process.env.EMAIL_FROM!;
const res = await resend.emails.send({
from: fromEmail,
to: [to],
subject,
html,
text,
});
if (res.error) throw res.error;
return res;
}
export async function sendWelcomeEmail(to: string, name?: string) {
const { default: WelcomeEmail } = await import("./templates/welcome");
const html = render(WelcomeEmail({ name }));
await sendMail({ to, subject: "Welcome to our app!", html });
}
Puntos clave:
- Mantén el código de Resend solo en servidor. Nunca expongas la API key en el navegador.
render()
genera HTML compatible con clientes de correo.
Dónde disparar los emails
Dispara desde eventos de servidor. Dos lugares comunes:
- Tras el registro de usuario (bienvenida)
- Este proyecto usa Better Auth (
src/lib/auth.ts
). Podemos enganchar el ciclo de vida de base de datos. - Añadiremos
databaseHooks.user.create.after
para llamar asendWelcomeEmail()
una vez guardado el usuario.
Ejemplo (dentro de la config de betterAuth({...})
):
databaseHooks: {
user: {
create: {
after: async (data) => {
try {
void (await import("@/services/email/send")).sendWelcomeEmail(data.email, data.nickname);
} catch (e) {
console.error("welcome email failed", e);
}
},
},
},
},
- Tras pago de Stripe (confirmación/recibo)
- Stripe notifica por webhook:
POST /api/pay/webhook/stripe
- En el handler para
checkout.session.completed
, llama asendMail()
para confirmar
Snippet en tu webhook de Stripe:
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const email = session.customer_details?.email;
if (email) {
await sendMail({
to: email,
subject: "Payment received",
html: `<p>Thanks for your purchase! Order: ${session.metadata?.order_no ?? session.id}</p>`
});
}
}
Consejo: No hagas esperar a Stripe. Responde el webhook rápido y envía el email en background (queueMicrotask
, setImmediate
o una cola). Stripe reintenta si el webhook falla — no si falla tu envío interno.
Cómo confirmar el envío
- Panel de Resend → Emails: logs de cada envío (destino, asunto, estado, errores)
- Eventos/Webhooks de Resend (opcional): recibir eventos de entrega/apertura/rebote
- Prueba manual: ruta de API temporal que te envíe un email de prueba
Ruta de prueba (runtime Node) en src/app/api/email-test/route.ts
:
export const runtime = "nodejs";
export async function POST() {
try {
const { sendWelcomeEmail } = await import("@/services/email/send");
await sendWelcomeEmail("you@example.com", "Friend");
return new Response("ok");
} catch (e) {
console.error(e);
return new Response("failed", { status: 500 });
}
}
Enviar petición:
curl -X POST http://localhost:3000/api/email-test
Revisa el panel de Resend para ver el email.
¿Necesito un worker/cola?
No para empezar. Puedes enviar dentro de la ruta API o del webhook. Para más fiabilidad y evitar timeouts:
- Usa un job/cola (Inngest, Trigger.dev o una cola basada en BD)
- O programa el envío tras responder a Stripe (p. ej.,
queueMicrotask
)
Empieza simple; añade cola luego si ves reintentos o lentitud.
Errores comunes
- Enviar desde un dominio no verificado: irá a spam o rebotará
- Poner la API key en el cliente: el navegador nunca debe verla
- Bloquear el webhook de Stripe esperando redes: responde primero, trabaja en background
- Emails duplicados por reintentos del webhook: guarda
event.id
para idempotencia
Lista rápida
- Cuenta de Resend + dominio verificado
-
RESEND_API_KEY
yEMAIL_FROM
en.env.local
-
@react-email/render
instalado -
src/services/email/
creado (envío + plantillas) - Disparadores conectados (signup + webhook de Stripe)
- Probado localmente y verificado en Resend
Accede a esta página en /:locale/blogs/email-service
durante el desarrollo.
Logs y Observabilidad
Logging estructurado para Node, Edge y Workers con IDs de solicitud, redacción de secretos y ejemplos por ruta. Funciona en Vercel, Cloudflare y servidores Node.
Configuración de la base de datos
Configura Postgres y Drizzle ORM — la columna vertebral para auth, cobros, almacenamiento, tareas y tablas de la app.