Generar reportes PDF a partir de datos de base de datos es una de las tareas más comunes en aplicaciones empresariales. Ya sea que estés construyendo un dashboard que exporta análisis mensuales, un CRM que produce resúmenes de clientes o un sistema ERP que imprime reportes de inventario, el flujo de trabajo es fundamentalmente el mismo: consultar la base de datos, transformar los resultados en HTML y renderizar ese HTML como PDF.
En este tutorial, recorreremos todo el proceso usando Node.js, PostgreSQL y la API HTML-a-PDF de TongoRender.
La Arquitectura
Antes de sumergirnos en el código, delineemos el flujo de datos:
- Consulta — Conéctate a tu base de datos y obtén los datos necesarios para el reporte.
- Transformación — Mapea los resultados de la consulta en una plantilla HTML con tablas, gráficos y estadísticas resumidas.
- Renderización — Envía el HTML a una API que lo convierte en PDF.
- Entrega — Guarda el PDF en disco, súbelo a almacenamiento en la nube o envíalo por correo al destinatario.
Paso 1: Configurando la Conexión a la Base de Datos
Usaremos la biblioteca pg para conectarnos a PostgreSQL:
npm install pg dotenv node-fetch
// 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 };
Paso 2: Consultando los Datos del Reporte
Supongamos que queremos un reporte mensual de ventas. Necesitamos ingresos totales, cantidad de pedidos y productos más vendidos:
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 };
}
Paso 3: Construyendo la Plantilla HTML
function buildReportHTML(data, year, month) {
const monthName = new Date(year, month - 1).toLocaleString('es', { month: 'long' });
return `
<!DOCTYPE html>
<html lang="es">
<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>Reporte de Ventas — ${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">${Number(data.summary.revenue).toLocaleString()}</div>
<div>Ingresos</div>
</div>
<div class="summary-card">
<div class="value">${Number(data.summary.avg_order_value).toFixed(2)}</div>
<div>Valor Promedio del Pedido</div>
</div>
</div>
<h2>Productos Más Vendidos</h2>
<table>
<thead><tr><th>Producto</th><th>Unidades</th><th>Ingresos</th></tr></thead>
<tbody>
${data.topProducts.map(p => \`
<tr><td>${p.name}</td><td>${p.units_sold}</td><td>${Number(p.revenue).toLocaleString()}</td></tr>
\`).join('')}
</tbody>
</table>
</body>
</html>`;
}
Paso 4: Convirtiendo a PDF con TongoRender
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(`Error de API: ${response.statusText}`);
return Buffer.from(await response.arrayBuffer());
}
Programando Reportes con Cron
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('finanzas@empresa.com', 'Reporte Mensual de Ventas', pdf);
});
Consejos para Reportes de Calidad
- Maneja datos vacíos con elegancia — Muestra mensajes "Sin datos disponibles" en lugar de tablas vacías.
- Formatea números consistentemente — Usa
Intl.NumberFormatpara moneda y números grandes. - Agrega timestamps — Incluye la fecha de generación y el rango de tiempo en el encabezado del reporte.
- Usa pool de conexiones —
pg.Poolgestiona conexiones eficientemente para solicitudes concurrentes.
TongoRender hace que la etapa de renderización de PDF sea sin esfuerzo. Concéntrate en consultar los datos correctos y diseñar una plantilla clara — la API se encarga del resto.
Comienza a generar reportes con TongoRender — 100 renderizaciones gratuitas al mes, sin necesidad de tarjeta de crédito.