🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
MÓDULO 2 - AULA 8

Sistema de Email

Emails transacionais com Resend - Magic Links e Templates profissionais

35 minutos
Intermediário
Sistema de Email do Restaurantix

O Restaurantix usa uma arquitetura de adapters para emails, permitindo trocar provedores facilmente.

Funcionalidades

  • Magic Links: Autenticação sem senha
  • Templates: HTML responsivo
  • Adapters: Múltiplos provedores
  • Fallback: Logs quando falha
  • Async: Não bloqueia a API

Arquitetura

  • Factory: Cria instâncias
  • Adapter: Interface comum
  • Service: Lógica de negócio
  • Provider: Resend API
  • Config: Variáveis de ambiente
Interface e Protocolo

protocols/emailAdapter.ts

// 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

Adapter do Resend

// 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();
};
Factory Pattern para Emails

factories/emailFactory.ts

// 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]());
};

Vantagens do Factory Pattern

// 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 })
};
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
Magic Links na Prática

Template HTML do Magic Link

// 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
);

Integração completa na rota

// 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" };
  }
);
Templates de Email Avançados

Sistema de Templates

// 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>
    `;
  }
}

Uso dos templates

// 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
);
Configuração e Boas Práticas

Variáveis de ambiente

# .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>"

Tratamento de erros

// 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
  }
}
🎯 Exercício Prático

Implemente o sistema completo de emails do Restaurantix:

Passos:

  1. Crie conta no Resend e configure domínio
  2. Implemente a interface EmailAdapter
  3. Crie o ResendAdapter com tratamento de erros
  4. Implemente o EmailService com Factory Pattern
  5. Crie templates HTML responsivos
  6. Integre com a rota de magic links
  7. Teste envio de emails em desenvolvimento
  8. Adicione logs e monitoramento

Teste o sistema:

# Solicitar magic link
curl -X POST http://localhost:3000/auth-links/authenticate \
-H "Content-Type: application/json" \
-d '{"email": "seu@email.com"}'
# Verificar logs do servidor
tail -f logs/email.log

💡 Dicas profissionais:

  • • Configure SPF, DKIM e DMARC para melhor entregabilidade
  • • Use templates responsivos que funcionam em todos os clientes
  • • Implemente rate limiting para evitar spam
  • • Monitore métricas: entrega, abertura, cliques
  • • Tenha fallback para quando o email falhar