How to Create PDF Reports from Database Data — TongoRender Blog
Back to Blog
tutorialsreportsdatabaseautomation

How to Create PDF Reports from Database Data

Learn how to connect to a database, query data, build an HTML report, and convert it to a polished PDF using an API. Includes step-by-step code examples in Node.js.

TongoRender TeamMarch 18, 202611 min

Generating PDF reports from database data is one of the most common tasks in business applications. Whether you are building a dashboard that exports monthly analytics, a CRM that produces client summaries, or an ERP system that prints inventory reports, the workflow is fundamentally the same: query your database, transform the results into HTML, and render that HTML as a PDF.

In this tutorial, we will walk through the complete process using Node.js, PostgreSQL, and TongoRender's HTML-to-PDF API.

The Architecture

Before diving into code, let us outline the data flow:

  1. Query — Connect to your database and fetch the data you need for the report.
  2. Transform — Map the raw query results into an HTML template with tables, charts, and summary statistics.
  3. Render — Send the HTML to an API that converts it into a PDF.
  4. Deliver — Save the PDF to disk, upload it to cloud storage, or email it to a recipient.

Step 1: Setting Up the Database Connection

We will use the pg library to connect to PostgreSQL. Install the dependencies:

npm install pg dotenv node-fetch

Create a database helper:

// 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 };

Step 2: Querying Report Data

Suppose we want a monthly sales report. We need total revenue, order count, top products, and a daily breakdown:

// 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 dailyBreakdown = await query(
    `SELECT DATE(created_at) as date,
            COUNT(*) as orders,
            SUM(total) as revenue
     FROM orders
     WHERE created_at BETWEEN $1 AND $2
     GROUP BY DATE(created_at)
     ORDER BY date`,
    [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, dailyBreakdown, topProducts };
}

Step 3: Building the HTML Template

Now we transform the data into a well-structured HTML document with print-friendly CSS:

function buildReportHTML(data, year, month) {
  const monthName = new Date(year, month - 1).toLocaleString('en', { month: 'long' });

  return `
  <!DOCTYPE html>
  <html>
  <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; }
      .summary-card .label { color: #666; margin-top: 5px; }
      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; }
      tr:nth-child(even) { background: #f8f9fa; }
      @media print { .summary-card { break-inside: avoid; } }
    </style>
  </head>
  <body>
    <h1>Monthly Sales Report — ${monthName} ${year}</h1>
    <div class="summary-grid">
      <div class="summary-card">
        <div class="value">${data.summary.order_count}</div>
        <div class="label">Total Orders</div>
      </div>
      <div class="summary-card">
        <div class="value">${Number(data.summary.revenue).toLocaleString()}</div>
        <div class="label">Revenue</div>
      </div>
      <div class="summary-card">
        <div class="value">${Number(data.summary.avg_order_value).toFixed(2)}</div>
        <div class="label">Average Order Value</div>
      </div>
    </div>

    <h2>Top Products</h2>
    <table>
      <thead><tr><th>Product</th><th>Units Sold</th><th>Revenue</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>`;
}

Step 4: Converting to PDF with TongoRender

With the HTML ready, a single API call produces the PDF:

const fetch = require('node-fetch');

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' },
      displayHeaderFooter: true,
      footerTemplate: '<div style="font-size:10px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
    }),
  });

  if (!response.ok) throw new Error(`API error: ${response.statusText}`);
  return Buffer.from(await response.arrayBuffer());
}

// Generate and save
const pdf = await generateReport(2026, 3);
fs.writeFileSync('reports/sales-march-2026.pdf', pdf);

Scheduling Reports with Cron

Automate report generation by scheduling a cron job that runs on the first of each month:

const cron = require('node-cron');

cron.schedule('0 8 1 * *', async () => {
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth(); // Previous month
  const pdf = await generateReport(year, month);
  await uploadToS3(pdf, `reports/${year}-${String(month).padStart(2, '0')}.pdf`);
  await sendEmail('finance@company.com', 'Monthly Sales Report', pdf);
});

Tips for Production-Quality Reports

  • Handle empty data gracefully — Show "No data available" messages instead of empty tables.
  • Format numbers consistently — Use Intl.NumberFormat for currency and large numbers.
  • Add timestamps — Include the generation date and time range in the report header.
  • Use connection pooling — The pg.Pool manages connections efficiently for concurrent requests.
  • Cache templates — If the HTML template does not change, compile it once and reuse for different data.

TongoRender makes the PDF rendering step effortless. Focus on querying the right data and designing a clear template — the API handles the rest.

Start generating reports with TongoRender — 100 free renders per month, no credit card required.

Share this articleShare on Twitter