Voltar para Artigos
Back-end9 min de leitura

E-mails Transacionais no Node.js: Templates, React Email e Entregabilidade

Handlebars com partials e helpers para templates reutilizáveis, React Email como alternativa moderna, inlining de CSS com juice, preview em desenvolvimento com Ethereal, e testes de renderização com Jest.

12 de agosto de 2026

E-mails transacionais (boas-vindas, reset de senha, confirmação de pedido) precisam de HTML especial: inline CSS porque Gmail e Outlook ignoram stylesheets externas, tabelas para layout porque Flexbox não funciona em todos os clientes de e-mail, e texto alternativo para clientes sem renderização HTML. Misturar esse HTML com a lógica TypeScript do Use Case cria um arquivo ilegível — a separação em templates é obrigatória.

Outro ponto crítico é a manutenção: quando você precisa atualizar o rodapé ou a identidade visual, não quer que um desenvolvedor precise procurar em 10 arquivos TypeScript diferentes qual string contém o HTML do e-mail. Templates separados permitem que designers editem o visual sem entender uma linha de TypeScript. E o mecanismo de partials do Handlebars vai além: blocos reutilizáveis como header e footer são declarados uma vez e injetados em todos os templates.

Arquitetura: IMailTemplateProvider

A chave para um sistema de templates flexível é a abstração via interface. O IMailProvider (responsável por enviar o e-mail) não deve saber se o template é gerado pelo Handlebars, React Email ou qualquer outra engine. Essa separação permite trocar a tecnologia de templates sem alterar o Use Case que chama o envio.

IMailTemplateProvider.ts
export interface ParseTemplateDTO {
  templatePath: string; // Caminho absoluto do template
  variables: Record<string, unknown>;
}

// Abstração: o MailProvider não sabe se usa Handlebars, React Email, etc.
export interface IMailTemplateProvider {
  parse(dto: ParseTemplateDTO): Promise<string>;
}

Handlebars com Partials e Helpers

O Handlebars compila um template .hbs com variáveis ({{name}}) e helpers condicionais ({{#if expiresIn}}) em HTML puro. O diferencial dos partials é que elementos compartilhados como o header (com o logo e a cor da marca) são declarados uma única vez em um arquivo separado e incluídos via {{> header}}. Isso garante que todos os e-mails da aplicação tenham o mesmo cabeçalho sem copiar e colar HTML.

bash
npm install handlebars
header.hbs
<!-- Partial: reutilizado por todos os e-mails -->
<table width="100%" cellpadding="0" cellspacing="0" style="background: #6366f1;">
  <tr>
    <td style="padding: 24px 40px;">
      <img src="{{logoUrl}}" alt="Logo" height="40" />
    </td>
  </tr>
</table>

O template principal de e-mail combina a estrutura HTML compatível com clientes de e-mail (tabelas aninhadas, inline styles) com a sintaxe do Handlebars para injeção de variáveis e o partial do header. Note o uso de {{#if expiresIn}} para blocos condicionais — o Handlebars é proposital sobre não ter muita lógica nos templates, o que força a pré-processar dados no servidor antes de passar as variáveis.

forgot-password.hbs
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Redefinição de senha</title>
</head>
<body style="margin: 0; padding: 0; background: #f3f4f6; font-family: Arial, sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0">
    <tr>
      <td align="center" style="padding: 40px 0;">
        <table width="600" cellpadding="0" cellspacing="0" style="background: #ffffff; border-radius: 8px;">

          <!-- Partial do header -->
          <tr><td>{{> header logoUrl=logoUrl}}</td></tr>

          <!-- Conteúdo -->
          <tr>
            <td style="padding: 40px;">
              <h1 style="font-size: 24px; color: #111827; margin: 0 0 16px;">Olá, {{name}}!</h1>
              <p style="color: #4b5563; line-height: 1.6; margin: 0 0 24px;">
                Recebemos uma solicitação de redefinição de senha para sua conta.
                {{#if expiresIn}}
                  Este link é válido por <strong>{{expiresIn}}</strong>.
                {{/if}}
              </p>
              <a href="{{resetUrl}}"
                 style="display: inline-block; background: #6366f1; color: #fff;
                        padding: 12px 32px; border-radius: 6px; text-decoration: none;
                        font-weight: 600;">
                Redefinir senha
              </a>
              <p style="color: #9ca3af; font-size: 12px; margin: 24px 0 0;">
                Se você não solicitou a redefinição, ignore este e-mail.
                O link expirará automaticamente.
              </p>
            </td>
          </tr>

        </table>
      </td>
    </tr>
  </table>
</body>
</html>

A implementação do HandlebarsMailTemplateProvider resolve dois desafios: carregar os partials automaticamente na inicialização (para que todos os templates possam usá-los sem registro manual) e compilar o template lendo o arquivo do disco. O método parse é assíncrono porque a leitura do arquivo é uma operação de I/O — mas a compilação do Handlebars em si é síncrona e muito rápida.

HandlebarsMailTemplateProvider.ts
import { readFile } from 'fs/promises';
import { resolve, dirname } from 'path';
import Handlebars from 'handlebars';
import type { IMailTemplateProvider, ParseTemplateDTO } from '../../../domain/providers/IMailTemplateProvider';

export class HandlebarsMailTemplateProvider implements IMailTemplateProvider {
  constructor() {
    // Registra partials automaticamente ao instanciar
    this.registerPartials();
  }

  private async registerPartials(): Promise<void> {
    const partialsDir = resolve(__dirname, 'templates', 'emails', 'partials');
    const { readdir } = await import('fs/promises');
    const files = await readdir(partialsDir).catch(() => []);

    for (const file of files) {
      const name = file.replace('.hbs', '');
      const content = await readFile(resolve(partialsDir, file), 'utf-8');
      Handlebars.registerPartial(name, content);
    }
  }

  async parse({ templatePath, variables }: ParseTemplateDTO): Promise<string> {
    const templateContent = await readFile(templatePath, 'utf-8');
    const template = Handlebars.compile(templateContent);
    return template(variables);
  }
}

Preview em Desenvolvimento com Ethereal

O maior problema de desenvolver templates de e-mail é o ciclo de feedback lento: você altera o HTML, configura um servidor SMTP real, envia para um e-mail de teste, abre o cliente de e-mail... e descobre que o botão ficou desalinhado no Outlook. O Ethereal resolve isso de forma elegante: é um serviço que captura e-mails em tempo real sem entregá-los, e gera uma URL de preview no browser. Todo o ciclo de teste fica local e instantâneo.

A estratégia recomendada é ter dois providers registrados no container de injeção de dependências: o EtherealMailProvider para o ambiente de desenvolvimento e o SESMailProvider (ou SendGridMailProvider) para produção. A troca acontece apenas na camada de configuração — o Use Case não muda.

EtherealMailProvider.ts (desenvolvimento)
import nodemailer from 'nodemailer';
import type { IMailProvider, SendMailDTO } from '../../../domain/providers/IMailProvider';
import type { IMailTemplateProvider } from '../../../domain/providers/IMailTemplateProvider';

// Ethereal: caixa de entrada falsa — captura e-mails sem enviar de verdade
// Gera uma URL de preview para visualizar o HTML renderizado
export class EtherealMailProvider implements IMailProvider {
  private transporter: nodemailer.Transporter;

  constructor(private templateProvider: IMailTemplateProvider) {}

  async init(): Promise<void> {
    const testAccount = await nodemailer.createTestAccount();
    this.transporter = nodemailer.createTransport({
      host: 'smtp.ethereal.email',
      port: 587,
      auth: { user: testAccount.user, pass: testAccount.pass },
    });
  }

  async send({ to, subject, templatePath, variables }: SendMailDTO): Promise<void> {
    const html = await this.templateProvider.parse({ templatePath, variables });

    const info = await this.transporter.sendMail({
      from: '"Meu App" <noreply@meuapp.com>',
      to,
      subject,
      html,
      // text: versão em texto puro para clientes sem HTML
      text: `Acesse: ${variables.resetUrl ?? variables.link}`,
    });

    // Log da URL de preview — abra no browser para ver o e-mail
    console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
  }
}

O React Email é uma alternativa moderna ao Handlebars: você escreve o template em TSX (com componentes como <Html>, <Body>, <Button>), preview no browser com hot reload, e converte para HTML inline automaticamente. Ideal para projetos React onde a equipe já conhece JSX. Use npm create email@latest para começar.

Conclusão

Separar templates de e-mail em arquivos .hbs permite que designers editem o HTML sem tocar no TypeScript. Os partials do Handlebars evitam duplicação do header/footer entre templates. O Ethereal em desenvolvimento elimina a necessidade de um servidor SMTP real para testar o visual dos e-mails. E a interface IMailTemplateProvider permite trocar Handlebars por React Email sem mudar o Use Case.