Melhores Práticas
Padrões e práticas recomendadas para usar o RenderHub de forma eficiente e segura
Este guia apresenta as melhores práticas para trabalhar com o RenderHub, desde design de templates até integração em produção.
Design de Templates
Estrutura Organizada
Organize seus templates com uma estrutura clara e modular:
<!-- ❌ Evite: Tudo em um único arquivo -->
<html>
<head><style>/* 500 linhas de CSS */</style></head>
<body><!-- 1000 linhas de HTML --></body>
</html>
<!-- ✅ Recomendado: CSS modular e componentes reutilizáveis -->
<html>
<head>
<link rel="stylesheet" href="styles/base.css">
<link rel="stylesheet" href="styles/components.css">
</head>
<body>
{{> header}}
{{> content}}
{{> footer}}
</body>
</html>
Nomenclatura de Variáveis
Use nomes descritivos e consistentes:
<!-- ❌ Evite: Nomes genéricos -->
{{d1}}
{{val}}
{{x}}
<!-- ✅ Recomendado: Nomes descritivos -->
{{invoiceDate}}
{{totalAmount}}
{{customerName}}
Convenções recomendadas:
- Use camelCase para variáveis:
firstName,totalAmount - Use prefixos para agrupamento:
customer.name,invoice.total - Seja específico:
createdAtem vez dedate
Valores Padrão
Sempre forneça valores padrão para evitar campos vazios:
<!-- ❌ Evite: Sem fallback -->
<p>Cliente: {{customerName}}</p>
<!-- ✅ Recomendado: Com valor padrão -->
<p>Cliente: {{customerName || "Não informado"}}</p>
<!-- ✅ Melhor ainda: Tratamento condicional -->
{{#if customerName}}
<p>Cliente: {{customerName}}</p>
{{else}}
<p class="warning">Cliente não informado</p>
{{/if}}
Formatação Consistente
Mantenha formatação consistente de dados:
<!-- Datas -->
{{formatDate createdAt "DD/MM/YYYY"}}
<!-- Moedas -->
{{formatCurrency totalAmount "BRL"}}
<!-- Números -->
{{formatNumber quantity decimals=0}}
Gestão de Dados
Validação de Entrada
Sempre valide os dados antes de enviar para o RenderHub:
// ❌ Evite: Sem validação
const data = req.body;
await renderHub.generatePDF(templateId, data);
// ✅ Recomendado: Com validação
interface InvoiceData {
customerName: string;
items: Array<{ name: string; price: number; quantity: number }>;
totalAmount: number;
dueDate: string;
}
function validateInvoiceData(data: any): data is InvoiceData {
return (
typeof data.customerName === 'string' &&
Array.isArray(data.items) &&
data.items.every(item =>
typeof item.name === 'string' &&
typeof item.price === 'number' &&
typeof item.quantity === 'number'
) &&
typeof data.totalAmount === 'number' &&
typeof data.dueDate === 'string'
);
}
if (!validateInvoiceData(data)) {
throw new Error('Dados inválidos');
}
const pdf = await renderHub.generatePDF(templateId, data);
Transformação de Dados
Use funções de mapeamento reutilizáveis:
// ✅ Crie mappers reutilizáveis
class InvoiceMapper {
static fromDatabase(dbInvoice: DatabaseInvoice): InvoiceData {
return {
customerName: dbInvoice.customer_name,
items: dbInvoice.line_items.map(item => ({
name: item.product_name,
price: item.unit_price,
quantity: item.qty,
})),
totalAmount: dbInvoice.total_amt,
dueDate: this.formatDate(dbInvoice.due_date),
};
}
private static formatDate(date: Date): string {
return date.toLocaleDateString('pt-BR');
}
}
// Uso
const dbInvoice = await db.invoices.findById(id);
const templateData = InvoiceMapper.fromDatabase(dbInvoice);
const pdf = await renderHub.generatePDF(templateId, templateData);
Dados Sensíveis
Nunca inclua dados sensíveis desnecessários:
// ❌ Evite: Enviando dados sensíveis desnecessários
const data = {
customer: {
name: user.name,
email: user.email,
password: user.password, // ❌ NUNCA!
creditCard: user.creditCard, // ❌ NUNCA!
}
};
// ✅ Recomendado: Apenas dados necessários
const data = {
customer: {
name: user.name,
email: user.email,
}
};
Performance
Reutilize Templates
Crie templates genéricos e reutilizáveis:
// ❌ Evite: Um template para cada caso
const template1 = await renderHub.createTemplate('invoice-customer-1', html);
const template2 = await renderHub.createTemplate('invoice-customer-2', html);
// ✅ Recomendado: Template genérico com variáveis
const template = await renderHub.createTemplate('invoice-generic', html);
// Reutilize para diferentes clientes
const pdf1 = await renderHub.generatePDF(template.id, customer1Data);
const pdf2 = await renderHub.generatePDF(template.id, customer2Data);
Cache de Templates
Mantenha cache local dos IDs de templates:
// ✅ Cache em memória
class TemplateCache {
private static cache = new Map<string, string>();
static async getTemplateId(name: string): Promise<string> {
if (this.cache.has(name)) {
return this.cache.get(name)!;
}
const template = await renderHub.getTemplateByName(name);
this.cache.set(name, template.id);
return template.id;
}
static clear() {
this.cache.clear();
}
}
// Uso
const templateId = await TemplateCache.getTemplateId('invoice');
const pdf = await renderHub.generatePDF(templateId, data);
Processamento em Lote
Para múltiplos PDFs, use processamento paralelo:
// ❌ Evite: Processamento sequencial
for (const invoice of invoices) {
const pdf = await renderHub.generatePDF(templateId, invoice);
await savePDF(pdf, invoice.id);
}
// ✅ Recomendado: Processamento paralelo
const pdfPromises = invoices.map(invoice =>
renderHub.generatePDF(templateId, invoice)
);
const pdfs = await Promise.all(pdfPromises);
await Promise.all(
pdfs.map((pdf, index) => savePDF(pdf, invoices[index].id))
);
Atenção: Respeite os limites de rate limiting da API.
Tratamento de Erros
Erros Específicos
Trate diferentes tipos de erros apropriadamente:
import { RenderHubError } from '@renderhub/sdk';
try {
const pdf = await renderHub.generatePDF(templateId, data);
return pdf;
} catch (error) {
if (error instanceof RenderHubError) {
switch (error.code) {
case 'TEMPLATE_NOT_FOUND':
logger.error('Template não encontrado', { templateId });
throw new NotFoundError('Template inválido');
case 'INVALID_DATA':
logger.warn('Dados inválidos', { data, error: error.message });
throw new ValidationError('Dados do template inválidos');
case 'RATE_LIMIT_EXCEEDED':
logger.warn('Rate limit excedido');
// Implementar retry com backoff exponencial
return await retryWithBackoff(() =>
renderHub.generatePDF(templateId, data)
);
case 'RENDER_TIMEOUT':
logger.error('Timeout no render', { templateId });
throw new TimeoutError('Geração de PDF demorou muito');
default:
logger.error('Erro desconhecido do RenderHub', { error });
throw new InternalError('Erro ao gerar PDF');
}
}
// Erro não relacionado ao RenderHub
logger.error('Erro inesperado', { error });
throw error;
}
Retry com Backoff Exponencial
Implemente retry para erros temporários:
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
initialDelay = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Não retry para erros permanentes
if (error instanceof RenderHubError) {
if (['TEMPLATE_NOT_FOUND', 'INVALID_DATA'].includes(error.code)) {
throw error;
}
}
// Aguardar com backoff exponencial
const delay = initialDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
Segurança
Proteção de API Keys
NUNCA exponha suas API keys:
// ❌ NUNCA faça isso
const apiKey = 'rh_live_abc123'; // Hard-coded
const client = new RenderHubClient({ apiKey });
// ❌ NUNCA faça isso
const apiKey = process.env.RENDERHUB_API_KEY;
res.json({ apiKey }); // Expondo no cliente
// ✅ Recomendado
const apiKey = process.env.RENDERHUB_API_KEY;
if (!apiKey) {
throw new Error('RENDERHUB_API_KEY não configurada');
}
const client = new RenderHubClient({ apiKey });
Sanitização de Entrada
Sempre sanitize dados de usuários:
import DOMPurify from 'isomorphic-dompurify';
// ❌ Evite: Dados não sanitizados
const data = {
customerName: req.body.name, // Pode conter scripts maliciosos
};
// ✅ Recomendado: Sanitizar entrada
const data = {
customerName: DOMPurify.sanitize(req.body.name),
notes: DOMPurify.sanitize(req.body.notes),
};
Validação de Upload
Se aceitar templates de usuários, valide rigorosamente:
function validateTemplateHTML(html: string): void {
// Verificar tamanho
const maxSize = 1024 * 1024; // 1MB
if (html.length > maxSize) {
throw new Error('Template muito grande');
}
// Bloquear tags perigosas
const dangerousTags = ['script', 'iframe', 'object', 'embed'];
const hasBlacklistedTags = dangerousTags.some(tag =>
html.toLowerCase().includes(`<${tag}`)
);
if (hasBlacklistedTags) {
throw new Error('Template contém tags não permitidas');
}
// Bloquear event handlers
const eventHandlers = ['onclick', 'onerror', 'onload'];
const hasEventHandlers = eventHandlers.some(handler =>
html.toLowerCase().includes(handler)
);
if (hasEventHandlers) {
throw new Error('Template contém event handlers não permitidos');
}
}
Monitoramento
Logging Estruturado
Use logging estruturado para facilitar debugging:
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'renderhub.log' }),
],
});
// ✅ Log estruturado
logger.info('PDF gerado com sucesso', {
templateId,
templateName: 'invoice',
dataSize: JSON.stringify(data).length,
duration: Date.now() - startTime,
userId: req.user.id,
});
logger.error('Falha ao gerar PDF', {
templateId,
error: error.message,
stack: error.stack,
data: data, // Cuidado com dados sensíveis
});
Métricas
Colete métricas importantes:
import { Counter, Histogram } from 'prom-client';
// Contador de PDFs gerados
const pdfGeneratedCounter = new Counter({
name: 'renderhub_pdfs_generated_total',
help: 'Total de PDFs gerados',
labelNames: ['template', 'status'],
});
// Histograma de duração
const pdfGenerationDuration = new Histogram({
name: 'renderhub_pdf_generation_duration_seconds',
help: 'Duração da geração de PDF',
labelNames: ['template'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
// Uso
const startTime = Date.now();
try {
const pdf = await renderHub.generatePDF(templateId, data);
pdfGeneratedCounter.inc({ template: templateName, status: 'success' });
return pdf;
} catch (error) {
pdfGeneratedCounter.inc({ template: templateName, status: 'error' });
throw error;
} finally {
const duration = (Date.now() - startTime) / 1000;
pdfGenerationDuration.observe({ template: templateName }, duration);
}
Versionamento
Versionamento de Templates
Mantenha versões de templates:
// ✅ Sistema de versionamento
const templateVersions = {
'invoice-v1': 'tmpl_abc123',
'invoice-v2': 'tmpl_def456',
'invoice-latest': 'tmpl_def456',
};
// Uso com fallback
async function generateInvoicePDF(data: InvoiceData, version = 'latest') {
const templateId = templateVersions[`invoice-${version}`];
if (!templateId) {
throw new Error(`Versão de template inválida: ${version}`);
}
return await renderHub.generatePDF(templateId, data);
}
Migração de Dados
Quando atualizar estrutura de dados, suporte versões antigas:
// ✅ Suporte a múltiplas versões
function normalizeInvoiceData(data: any): InvoiceDataV2 {
// Detectar versão
if (data.version === 2 || data.newField) {
return data as InvoiceDataV2;
}
// Converter v1 para v2
return {
version: 2,
customerName: data.customer_name, // Campo renomeado
items: data.items,
totalAmount: data.total,
dueDate: data.due_date,
newField: 'default value', // Novo campo
};
}
Testes
Testes Unitários
Teste a transformação de dados:
import { describe, it, expect } from 'vitest';
describe('InvoiceMapper', () => {
it('deve mapear invoice do banco para template', () => {
const dbInvoice = {
id: 1,
customer_name: 'João Silva',
total_amt: 1500.00,
due_date: new Date('2024-12-31'),
};
const result = InvoiceMapper.fromDatabase(dbInvoice);
expect(result).toEqual({
customerName: 'João Silva',
totalAmount: 1500.00,
dueDate: '31/12/2024',
});
});
it('deve lidar com dados faltantes', () => {
const dbInvoice = {
id: 1,
customer_name: null,
total_amt: 0,
due_date: null,
};
const result = InvoiceMapper.fromDatabase(dbInvoice);
expect(result.customerName).toBe('Não informado');
expect(result.totalAmount).toBe(0);
expect(result.dueDate).toBe('N/A');
});
});
Testes de Integração
Teste a geração de PDFs:
describe('PDF Generation', () => {
it('deve gerar PDF de invoice', async () => {
const data = {
customerName: 'Teste Cliente',
items: [
{ name: 'Produto A', price: 100, quantity: 2 },
],
totalAmount: 200,
dueDate: '31/12/2024',
};
const pdf = await renderHub.generatePDF('invoice', data);
expect(pdf).toBeInstanceOf(Buffer);
expect(pdf.length).toBeGreaterThan(0);
// Verificar que é um PDF válido
expect(pdf.toString('utf8', 0, 4)).toBe('%PDF');
});
});
Testes de Snapshot
Use snapshots para templates HTML:
import { describe, it, expect } from 'vitest';
import { renderTemplate } from './template-renderer';
describe('Invoice Template', () => {
it('deve renderizar template corretamente', () => {
const data = {
invoiceNumber: 'INV-001',
customerName: 'João Silva',
items: [
{ name: 'Item 1', price: 100, quantity: 2 },
],
totalAmount: 200,
};
const html = renderTemplate('invoice', data);
expect(html).toMatchSnapshot();
});
});
Checklist de Lançamento
Antes de colocar em produção, verifique:
- API keys estão em variáveis de ambiente
- Validação de dados implementada
- Tratamento de erros apropriado
- Retry logic para erros temporários
- Logging estruturado configurado
- Métricas sendo coletadas
- Rate limiting considerado
- Templates testados com dados reais
- Sanitização de entrada implementada
- Documentação atualizada
- Testes automatizados passando
- Monitoramento configurado
- Plano de rollback preparado
Próximos Passos
- Leia o guia de Performance para otimizações avançadas
- Confira o guia de Segurança para proteções adicionais
- Veja o guia de Caching para melhorar performance