Gerar relatórios PDF a partir de dados do banco de dados é uma das tarefas mais comuns em aplicações empresariais. Seja construindo um dashboard que exporta análises mensais, um CRM que produz resumos de clientes ou um sistema ERP que imprime relatórios de inventário, o fluxo de trabalho é fundamentalmente o mesmo: consultar o banco de dados, transformar os resultados em HTML e renderizar esse HTML como PDF.
Neste tutorial, vamos percorrer todo o processo usando Node.js, PostgreSQL e a API HTML-para-PDF do TongoRender.
A Arquitetura
Antes de mergulhar no código, vamos delinear o fluxo de dados:
- Consulta — Conecte ao banco de dados e busque os dados necessários para o relatório.
- Transformação — Mapeie os resultados da consulta em um template HTML com tabelas, gráficos e estatísticas resumidas.
- Renderização — Envie o HTML para uma API que o converte em PDF.
- Entrega — Salve o PDF em disco, faça upload para armazenamento em nuvem ou envie por email ao destinatário.
Passo 1: Configurando a Conexão com o Banco de Dados
Vamos usar a biblioteca pg para conectar ao PostgreSQL. Instale as dependências:
npm install pg dotenv node-fetch
Crie um helper para o banco de dados:
// db.js
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
async function query(text, params) {
const result = await pool.query(text, params);
return result.rows;
}
module.exports = { query };
Passo 2: Consultando os Dados do Relatório
Suponha que queremos um relatório mensal de vendas. Precisamos de receita total, contagem de pedidos, produtos mais vendidos e um detalhamento diário:
// reports/monthly-sales.js
const { query } = require('../db');
async function getMonthlySalesData(year, month) {
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
const [summary] = await query(
`SELECT COUNT(*) as order_count,
SUM(total) as revenue,
AVG(total) as avg_order_value
FROM orders
WHERE created_at BETWEEN $1 AND $2`,
[startDate, endDate]
);
const topProducts = await query(
`SELECT p.name, SUM(oi.quantity) as units_sold, SUM(oi.subtotal) as revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN orders o ON o.id = oi.order_id
WHERE o.created_at BETWEEN $1 AND $2
GROUP BY p.name
ORDER BY revenue DESC
LIMIT 10`,
[startDate, endDate]
);
return { summary, topProducts };
}
Passo 3: Construindo o Template HTML
Agora transformamos os dados em um documento HTML bem estruturado com CSS amigável para impressão:
function buildReportHTML(data, year, month) {
const monthName = new Date(year, month - 1).toLocaleString('pt-BR', { month: 'long' });
return `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<style>
body { font-family: 'Helvetica Neue', sans-serif; color: #1a1a2e; margin: 40px; }
h1 { color: #16213e; border-bottom: 3px solid #0f3460; padding-bottom: 10px; }
.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
.summary-card { background: #f0f4ff; border-radius: 8px; padding: 20px; text-align: center; }
.summary-card .value { font-size: 2em; font-weight: 700; color: #0f3460; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th { background: #0f3460; color: white; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #e0e0e0; }
</style>
</head>
<body>
<h1>Relatório de Vendas — ${monthName} ${year}</h1>
<div class="summary-grid">
<div class="summary-card">
<div class="value">${data.summary.order_count}</div>
<div>Total de Pedidos</div>
</div>
<div class="summary-card">
<div class="value">R$ ${Number(data.summary.revenue).toLocaleString('pt-BR')}</div>
<div>Receita</div>
</div>
<div class="summary-card">
<div class="value">R$ ${Number(data.summary.avg_order_value).toFixed(2)}</div>
<div>Valor Médio do Pedido</div>
</div>
</div>
<h2>Produtos Mais Vendidos</h2>
<table>
<thead><tr><th>Produto</th><th>Unidades</th><th>Receita</th></tr></thead>
<tbody>
${data.topProducts.map(p => \`
<tr><td>${p.name}</td><td>${p.units_sold}</td><td>R$ ${Number(p.revenue).toLocaleString('pt-BR')}</td></tr>
\`).join('')}
</tbody>
</table>
</body>
</html>`;
}
Passo 4: Convertendo para PDF com o TongoRender
Com o HTML pronto, uma única chamada de API produz o PDF:
async function generateReport(year, month) {
const data = await getMonthlySalesData(year, month);
const html = buildReportHTML(data, year, month);
const response = await fetch('https://api.tongorender.io/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.TONGORENDER_API_KEY,
},
body: JSON.stringify({
html,
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
}),
});
if (!response.ok) throw new Error(`Erro da API: ${response.statusText}`);
return Buffer.from(await response.arrayBuffer());
}
Agendando Relatórios com Cron
Automatize a geração de relatórios agendando um job cron que executa no primeiro dia de cada mês:
const cron = require('node-cron');
cron.schedule('0 8 1 * *', async () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const pdf = await generateReport(year, month);
await uploadToS3(pdf, `reports/${year}-${String(month).padStart(2, '0')}.pdf`);
await sendEmail('financeiro@empresa.com', 'Relatório Mensal de Vendas', pdf);
});
Dicas para Relatórios de Qualidade
- Trate dados vazios com elegância — Mostre mensagens "Sem dados disponíveis" em vez de tabelas vazias.
- Formate números consistentemente — Use
Intl.NumberFormatpara moeda e números grandes. - Adicione timestamps — Inclua a data de geração e o período no cabeçalho do relatório.
- Use pool de conexões — O
pg.Poolgerencia conexões eficientemente para requisições concorrentes.
O TongoRender torna a etapa de renderização do PDF sem esforço. Concentre-se em consultar os dados certos e projetar um template claro — a API cuida do resto.
Comece a gerar relatórios com o TongoRender — 100 renderizações gratuitas por mês, sem necessidade de cartão de crédito.