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.
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.
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.
npm install handlebars<!-- 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.
<!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.
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.
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.