Emails transacionais com Resend - Magic Links e Templates profissionais
O Restaurantix usa uma arquitetura de adapters para emails, permitindo trocar provedores facilmente.
// src/application/infra/messaging/protocols/emailAdapter.ts
export interface EmailAdapter {
sendEmail(to: string, subject: string, html: string): Promise<any>;
}
// Interface simples e flexível
// Permite implementar qualquer provedor de email
// Retorna Promise para operações assíncronas// src/application/infra/messaging/adapters/resendAdapter.ts
import { Resend } from "resend";
import type { EmailAdapter } from "../protocols/emailAdapter";
import { env } from "@/env";
const resend = new Resend(env.RESEND_API_KEY);
export class ResendAdapter implements EmailAdapter {
async sendEmail(to: string, subject: string, html: string): Promise<any> {
const { data, error } = await resend.emails.send({
from: "Restaurantix <noreply@restaurantix.com>",
to: [to],
subject,
html
});
if (error) {
console.log({ error });
return error;
}
console.log(data);
return data;
}
}
export const makeResendAdapter = () => {
return new ResendAdapter(); // src/application/infra/messaging/factories/emailFactory.ts
import { makeResendAdapter } from "../adapters/resendAdapter";
import type { EmailAdapter } from "../protocols/emailAdapter";
export class EmailService {
private adapter: EmailAdapter;
constructor(adapter: EmailAdapter) {
this.adapter = adapter;
}
async sendEmail(to: string, subject: string, html: string): Promise<any> {
await this.adapter.sendEmail(to, subject, html);
}
}
type EmailProviderKeys = "resend" | "sendgrid" | "mailgun";
const emailProviders: Record<EmailProviderKeys, () => EmailAdapter> = {
resend: makeResendAdapter,
// sendgrid: makeSendGridAdapter, // Futuro
// mailgun: makeMailgunAdapter, // Futuro export const makeEmailService = () => {
const provider = (process.env.EMAIL_PROVIDER || "resend") as EmailProviderKeys;
return new EmailService(emailProviders[provider]()); // 1. FLEXIBILIDADE - Trocar provedor via env
EMAIL_PROVIDER=resend # Usar Resend
EMAIL_PROVIDER=sendgrid # Usar SendGrid
EMAIL_PROVIDER=mailgun # Usar Mailgun
// 2. TESTABILIDADE - Mock fácil
const mockAdapter = {
sendEmail: jest.fn().mockResolvedValue({ success: true })
};
export default function Aula8() {
const emailService = new EmailService(mockAdapter);
// 3. EXTENSIBILIDADE - Adicionar novos provedores
const emailProviders = {
resend: makeResendAdapter,
sendgrid: makeSendGridAdapter, // ✅ Novo
mailgun: makeMailgunAdapter, // ✅ Novo
postmark: makePostmarkAdapter, // ✅ Novo // 4. SINGLE RESPONSIBILITY - Cada adapter faz uma coisa
// ResendAdapter → Só integra com Resend
// SendGridAdapter → Só integra com SendGrid
// EmailService → Só orquestra o envio// Template usado na rota send-auth-link.ts
const magicLinkTemplate = `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #4a5568;">Welcome to Restaurantix</h1>
<p>Click the button below to sign in to your account:</p>
<a
href="${authLink.toString()}"
style="display: inline-block; background-color: #3182ce; color: white; font-weight: bold; padding: 12px 24px; border-radius: 4px; text-decoration: none;"
>
Sign In
</a>
<p style="margin-top: 24px; color: #718096; font-size: 14px;">
If you didn't request this link, you can safely ignore this email.
</p>
</div>
`;
// Uso na rota
await emailSender.sendEmail(
email,
"Authentication Link",
magicLinkTemplate
);// Trecho da rota send-auth-link.ts
export const sendAuthLink = csrf.post(
"/auth-links/authenticate",
async ({ body }) => {
const { email } = body;
// 1. Verificar se usuário existe
const [userFound] = await db
.select()
.from(users)
.where(eq(users.email, email));
if (!userFound) {
return { message: "Authentication link sent" };
}
// 2. Gerar código único
const code = createId();
// 3. Salvar no banco com expiração
await db.insert(authLinks).values({
userId: userFound.id,
code,
expiresAt: dayjs().add(1, "days").toDate()
});
// 4. Construir URL do magic link
const authLink = new URL(`auth-links/authenticate`, env.API_BASE_URL);
authLink.searchParams.set("code", code);
authLink.searchParams.set("redirect", env.AUTH_REDIRECT_URL);
// 5. ENVIAR EMAIL (se configurado)
if (env.RESEND_API_KEY) {
try {
const emailSender = makeEmailService();
await emailSender.sendEmail(
email,
"Authentication Link",
magicLinkTemplate
);
} catch (error) {
console.error("Failed to send authentication email:", error);
// NÃO falha a requisição se email falhar
}
}
return { message: "Authentication link sent" };
}
);// src/application/infra/messaging/templates/emailTemplates.ts
export class EmailTemplates {
static magicLink(authLink: string, userName?: string) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Restaurantix - Magic Link</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f7fafc;">
<div style="max-width: 600px; margin: 0 auto; background-color: white;">
<!-- Header -->
<div style="background-color: #2d3748; padding: 20px; text-align: center;">
<h1 style="color: #68d391; margin: 0; font-size: 24px;">🍽️ Restaurantix</h1>
</div>
<!-- Content -->
<div style="padding: 40px 20px;">
<h2 style="color: #2d3748; margin-bottom: 20px;">
${userName ? `Olá, ${userName}!` : 'Olá!'}
</h2>
<p style="color: #4a5568; line-height: 1.6; margin-bottom: 30px;">
Clique no botão abaixo para acessar sua conta no Restaurantix.
Este link é válido por 24 horas.
</p>
<!-- CTA Button -->
<div style="text-align: center; margin: 40px 0;">
<a href="${authLink}"
style="display: inline-block; background-color: #68d391; color: white;
text-decoration: none; padding: 15px 30px; border-radius: 8px;
font-weight: bold; font-size: 16px;">
Acessar Conta
</a>
</div>
<p style="color: #718096; font-size: 14px; line-height: 1.6;">
Se você não solicitou este acesso, pode ignorar este email com segurança.
<br><br>
<strong>Link direto:</strong><br>
<a href="${authLink}" style="color: #3182ce; word-break: break-all;">
${authLink}
</a>
</p>
</div>
<!-- Footer -->
<div style="background-color: #edf2f7; padding: 20px; text-align: center;">
<p style="color: #718096; font-size: 12px; margin: 0;">
© {new Date().getFullYear()} Restaurantix. Todos os direitos reservados.
</p>
</div>
</div>
</body>
</html>
`;
}
static orderConfirmation(orderData: any) {
return `
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif;">
<h2>Pedido Confirmado! 🎉</h2>
<p>Seu pedido #${orderData.id} foi confirmado.</p>
<p><strong>Total:</strong> R$ ${(orderData.total / 100).toFixed(2)}</p>
<p><strong>Status:</strong> ${orderData.status}</p>
</body>
</html>
`;
}
static passwordReset(resetLink: string) {
return `
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif;">
<h2>Redefinir Senha</h2>
<p>Clique no link abaixo para redefinir sua senha:</p>
<a href="${resetLink}">Redefinir Senha</a>
</body>
</html>
`;
}
}// Na rota send-auth-link.ts
import { EmailTemplates } from "@/application/infra/messaging/templates/emailTemplates";
// Usar template profissional
const htmlContent = EmailTemplates.magicLink(
authLink.toString(),
userFound.name
);
await emailSender.sendEmail(
email,
"Acesse sua conta - Restaurantix",
htmlContent
);
// Para outros tipos de email
const orderHtml = EmailTemplates.orderConfirmation({
id: "order-123",
total: 2590,
status: "confirmed"
});
await emailSender.sendEmail(
customerEmail,
"Pedido Confirmado - Restaurantix",
orderHtml
);# .env RESEND_API_KEY="re_123456789" EMAIL_PROVIDER="resend" API_BASE_URL="https://api.restaurantix.com" AUTH_REDIRECT_URL="https://app.restaurantix.com" # Configuração do domínio no Resend # 1. Adicionar domínio: restaurantix.com # 2. Configurar DNS records # 3. Verificar domínio # 4. Usar: "Restaurantix <noreply@restaurantix.com>"
// Implementação robusta com fallbacks
export class EmailService {
async sendEmail(to: string, subject: string, html: string) {
try {
const result = await this.adapter.sendEmail(to, subject, html);
// Log de sucesso
console.log(`✅ Email sent to ${to}: ${subject}`);
return result;
} catch (error) {
// Log de erro detalhado
console.error(`❌ Failed to send email to ${to}:`, {
subject,
error: error.message,
timestamp: new Date().toISOString()
});
// Não quebrar a aplicação
// Pode implementar retry logic aqui
throw error;
}
}
}
// Na rota, sempre usar try/catch
if (env.RESEND_API_KEY) {
try {
const emailSender = makeEmailService();
await emailSender.sendEmail(email, subject, html);
} catch (error) {
console.error("Email failed, but continuing:", error);
// Não falha a requisição principal
}
}Implemente o sistema completo de emails do Restaurantix:
# Solicitar magic linkcurl -X POST http://localhost:3000/auth-links/authenticate \ -H "Content-Type: application/json" \ -d '{"email": "seu@email.com"}'# Verificar logs do servidortail -f logs/email.log