Guides

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: createdAt em vez de date

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

On this page