Resend en Producción: Patrones Avanzados que el Tutorial Básico No Te Enseña
Patrones avanzados de Resend para producción: webhooks con verificación de firma, colas con reintentos, templates tipados con React Email y batch sending.
Tu Setup Básico de Resend Falla en Producción. Aquí Está Por Qué
Tienes la API key. Enviaste tu primer email. Funciona en local.
Luego llegas a producción y empiezan los problemas reales.
Emails que se pierden sin error visible. Webhooks que no se procesan. Templates que explotan con datos inesperados.
*El problema real del email no es enviarlo. Es garantizar que llega, que se procesa, y que sabes cuándo falla.*
Esto es lo que el tutorial básico de Resend no te cuenta.
---
Patrón #1: Webhooks con Verificación de Firma
La mayoría de developers conectan el webhook de Resend directamente a su API sin verificar la firma.
Cualquiera puede hacer un POST a tu endpoint y simular eventos falsos.
❌ Enfoque peligroso:
```typescript
// pages/api/webhooks/resend.ts
export async function POST(req: Request) {
const payload = await req.json();
// Procesar sin verificar — NUNCA hagas esto
await handleEmailEvent(payload);
return Response.json({ ok: true });
}
```
✅ Verificación de firma con Svix (que usa Resend internamente):
```typescript
import { Webhook } from 'svix';
export async function POST(req: Request) {
const webhookSecret = process.env.RESEND_WEBHOOK_SECRET!;
const svixId = req.headers.get('svix-id');
const svixTimestamp = req.headers.get('svix-timestamp');
const svixSignature = req.headers.get('svix-signature');
if (!svixId || !svixTimestamp || !svixSignature) {
return new Response('Missing headers', { status: 400 });
}
const body = await req.text();
const wh = new Webhook(webhookSecret);
let event;
try {
event = wh.verify(body, {
'svix-id': svixId,
'svix-timestamp': svixTimestamp,
'svix-signature': svixSignature,
});
} catch {
return new Response('Invalid signature', { status: 401 });
}
await handleEmailEvent(event as ResendWebhookEvent);
return Response.json({ ok: true });
}
```
Instala `svix` como dependencia. Resend firma todos los webhooks con este estándar.
El secret lo encuentras en el dashboard de Resend bajo tu webhook endpoint.
---
Patrón #2: Cola de Emails con Reintentos Inteligentes
Enviar emails directamente desde una Server Action o API route es el error más común en producción.
Si la llamada a Resend falla, pierdes el email para siempre.
El patrón correcto usa una cola de trabajos.
Implementación con Inngest
```typescript
// inngest/functions/send-email.ts
import { inngest } from '../client';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export const sendEmailFunction = inngest.createFunction(
{
id: 'send-email',
retries: 4,
// Backoff exponencial automático
},
{ event: 'email/send' },
async ({ event, step }) => {
const { to, subject, template, data } = event.data;
const result = await step.run('send-via-resend', async () => {
const { data: emailData, error } = await resend.emails.send({
from: 'noreply@tudominio.com',
to,
subject,
react: renderTemplate(template, data),
});
if (error) throw new Error(error.message);
return emailData;
});
return { emailId: result?.id, status: 'sent' };
}
);
```
```typescript
// En tu Server Action o API route:
await inngest.send({
name: 'email/send',
data: {
to: user.email,
subject: 'Bienvenido a la plataforma',
template: 'welcome',
data: { name: user.name },
},
});
```
Resultado: Si Resend devuelve un error 5xx, Inngest reintenta automáticamente con backoff exponencial. Cero emails perdidos.
---
Patrón #3: Templates con React Email y Type Safety
*El real problema de los templates de email no es el diseño. Es que explotan silenciosamente con props inesperadas.*
La solución es tipar completamente tus componentes de React Email.
```typescript
// emails/templates/welcome.tsx
import {
Html, Head, Body, Container,
Heading, Text, Button, Section
} from '@react-email/components';
interface WelcomeEmailProps {
name: string;
confirmUrl: string;
expiresInHours?: number;
}
export function WelcomeEmail({
name,
confirmUrl,
expiresInHours = 24,
}: WelcomeEmailProps) {
return (
<Html lang="es">
<Head />
<Body style={bodyStyle}>
<Container>
<Heading>Hola, {name}</Heading>
<Text>
Tu enlace de confirmación expira en {expiresInHours} horas.
</Text>
<Section>
<Button href={confirmUrl} style={buttonStyle}>
Confirmar cuenta
</Button>
</Section>
</Container>
</Body>
</Html>
);
}
const bodyStyle = { backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' };
const buttonStyle = { backgroundColor: '#0070f3', color: '#fff', padding: '12px 20px' };
```
```typescript
// Envío tipado — TypeScript te avisa si falta una prop
await resend.emails.send({
from: 'noreply@tudominio.com',
to: user.email,
subject: 'Confirma tu cuenta',
react: <WelcomeEmail
name={user.name}
confirmUrl={`https://tudominio.com/confirm?token=${token}`}
/>,
});
```
Con este patrón, un cambio en la interfaz del template rompe en compilación, no en producción a las 3am.
---
Patrón #4: Monitorización de Deliverability con Webhooks
Saber que enviaste el email no es suficiente. Necesitas saber si llegó.
Resend emite estos eventos via webhook:
→ `email.sent` — El email salió de Resend
→ `email.delivered` — El servidor destino lo aceptó
→ `email.opened` — El destinatario lo abrió (requiere tracking pixel)
→ `email.bounced` — Rebote permanente — *elimina este email de tu lista*
→ `email.complained` — Marcado como spam — *acción urgente requerida*
```typescript
type ResendWebhookEvent = {
type: 'email.sent' | 'email.delivered' | 'email.opened'
| 'email.bounced' | 'email.complained' | 'email.clicked';
data: {
email_id: string;
from: string;
to: string[];
subject: string;
created_at: string;
};
};
async function handleEmailEvent(event: ResendWebhookEvent) {
switch (event.type) {
case 'email.bounced':
// Marcar email como inválido en tu BD
await db.users.updateMany({
where: { email: { in: event.data.to } },
data: { emailStatus: 'bounced' },
});
break;
case 'email.complained':
// Desuscribir inmediatamente — protege tu reputación
await unsubscribeUsers(event.data.to);
break;
case 'email.delivered':
// Actualizar métricas de deliverability
await updateDeliveryMetrics(event.data.email_id);
break;
}
}
```
Ignorar bounces y complaints destruye tu reputación de dominio. Los ISPs como Gmail y Outlook penalizan dominios con tasa alta de complaints. Un complaint rate por encima del 0,1% activa filtros automáticos.
---
Patrón #5: Batch Sending para Notificaciones Masivas
Cuando necesitas enviar emails a miles de usuarios, no hagas un bucle con `resend.emails.send` en cada iteración.
❌ Enfoque que agota el rate limit:
```typescript
for (const user of users) {
await resend.emails.send({ to: user.email, ... });
// Bloquea el thread. Explota con 1000 usuarios.
}
```
✅ Batch con control de concurrencia:
```typescript
import pLimit from 'p-limit';
const limit = pLimit(10); // Máximo 10 requests simultáneos
async function sendBulkEmails(users: User[]) {
const tasks = users.map(user =>
limit(() =>
resend.emails.send({
from: 'noreply@tudominio.com',
to: user.email,
subject: 'Actualización importante',
react: <NotificationEmail name={user.name} />,
})
)
);
const results = await Promise.allSettled(tasks);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error(`${failed.length} emails fallidos`, failed);
// Reencolar los fallidos via Inngest
}
return results;
}
```
`p-limit` controla la concurrencia. `Promise.allSettled` captura fallos individuales sin abortar el batch completo.
---
Variables de Entorno para un Setup Completo
```bash
.env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
RESEND_FROM_EMAIL=noreply@tudominio.com
RESEND_FROM_NAME="Tu Producto"
Para React Email preview server
RESEND_AUDIENCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
Nunca hardcodees la API key. Nunca la incluyas en client-side code — Next.js server-only o Edge Functions exclusivamente.
---
Resumen: El Setup de Producción Completo
→ Verifica firmas en todos los webhooks con `svix` — seguridad no opcional
→ Usa una cola (Inngest, Trigger.dev, o BullMQ) — cero emails perdidos por fallos transitorios
→ Tipa tus templates con React Email — errores en compilación, no en runtime
→ Procesa bounces y complaints — tu reputación de dominio depende de esto
→ Controla concurrencia en envíos masivos con `p-limit` — respeta los rate limits
*El email transaccional bien implementado es infraestructura invisible: nadie lo nota cuando funciona. Todo el mundo lo nota cuando falla.*
Resend te da las herramientas. Estos patrones garantizan que las uses correctamente en producción real.
Lee el artículo completo en brianmenagomez.com


