Start free

· 8 min read

Generate PDF Reports in Node.js without Puppeteer

Skip Puppeteer and Chromium: generate PDF reports from Node.js with a lightweight REST call to QuartzAPI and serve the buffer from Express.

Puppeteer and headless Chrome instances are notorious resource hogs in Node.js environments. Running Chromium inside Docker containers inflates image sizes, consumes high RAM (often causing out-of-memory crashes on low-tier cloud instances), and adds severe cold-start latency.

By switching to QuartzAPI, you eliminate headless browser dependencies entirely. Your Node.js service sends report JSON over REST; the cloud engine renders the PDF from a Visual Builder template.

Prerequisites

  • Node.js v16+ (CommonJS or ES Modules)
  • node-fetch or native fetch (built-in from Node v18+)
  • A QuartzAPI API key and an active template code (e.g. SALES_REPORT_Q3)

Step 1: install dependencies (optional)

If you use native fetch (Node 18+), no extra packages are required for the HTTP call. For older Node versions, install node-fetch:

npm install node-fetch

Express is only needed if you expose an HTTP route that returns the PDF to the browser (npm install express).

Step 2: build the API integration service

Create a dedicated module (pdfService.js) that calls generate-document, then downloads the PDF bytes. QuartzAPI returns JSON with documentId / downloadUrl — not a raw PDF body on the first request.

// pdfService.js
const QUARTZ_GENERATE_URL =
  'https://backend.quartzapi.com/index.php?r=api/v1-jobs/generate-document';
const QUARTZ_DOWNLOAD_URL =
  'https://backend.quartzapi.com/index.php?r=api/v1-documents/download';
const API_KEY = process.env.QUARTZ_API_KEY;

/**
 * Generates a PDF buffer from dynamic report data.
 * @param {string} templateCode - Visual Builder template code.
 * @param {object} reportData - Report parameters (typically under master / items).
 * @returns {Promise<Buffer>}
 */
async function generateReportPdf(templateCode, reportData) {
  const generateRes = await fetch(QUARTZ_GENERATE_URL, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      templateCode,
      outputFormat: 'pdf',
      externalId: reportData.externalId || undefined,
      data: reportData,
    }),
  });

  if (!generateRes.ok) {
    const errorText = await generateRes.text();
    throw new Error(`QuartzAPI Error [${generateRes.status}]: ${errorText}`);
  }

  const payload = await generateRes.json();
  const documentId = payload?.result?.documentId;
  const downloadUrl =
    payload?.result?.downloadUrl ||
    `${QUARTZ_DOWNLOAD_URL}&uid=${encodeURIComponent(documentId || '')}`;

  if (!documentId && !payload?.result?.downloadUrl) {
    throw new Error('QuartzAPI response missing documentId / downloadUrl');
  }

  const pdfRes = await fetch(downloadUrl, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
    },
  });

  if (!pdfRes.ok) {
    const errorText = await pdfRes.text();
    throw new Error(`QuartzAPI download Error [${pdfRes.status}]: ${errorText}`);
  }

  const arrayBuffer = await pdfRes.arrayBuffer();
  return Buffer.from(arrayBuffer);
}

module.exports = { generateReportPdf };

Bind placeholders in the Visual Builder to keys such as master.report_title and iterate Details from master.items (or your mapped array). More on the JSON shape: generate PDF invoices from JSON.

Step 3: Express.js controller example

Serve the generated report dynamically from an Express HTTP route:

// server.js
const express = require('express');
const { generateReportPdf } = require('./pdfService');

const app = express();

app.get('/reports/monthly-sales', async (req, res) => {
  try {
    const salesData = {
      master: {
        report_title: 'Monthly Sales Analytics - Q3 2026',
        generated_at: new Date().toISOString(),
        total_revenue: '$45,210.00',
        active_subscriptions: 312,
        churn_rate: '1.2%',
        items: [
          { name: 'Enterprise Plan', sales: 120, revenue: '$24,000' },
          { name: 'Pro Plan', sales: 192, revenue: '$21,210' },
        ],
      },
    };

    const pdfBuffer = await generateReportPdf('SALES_REPORT_Q3', salesData);

    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader(
      'Content-Disposition',
      'inline; filename=sales-report.pdf'
    );
    res.send(pdfBuffer);
  } catch (error) {
    console.error('PDF Generation Failed:', error);
    res.status(500).json({ error: 'Failed to generate report' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Set QUARTZ_API_KEY in the environment (never hard-code secrets in source). Use Content-Disposition: attachment if you prefer a forced download instead of inline preview.

Why ditch Puppeteer for an API?

Metric Puppeteer / Headless Chrome QuartzAPI
Docker image size ~1 GB+ (includes Chromium) ~50 MB (standard Node)
RAM usage ~100–500 MB per render ~2 MB (HTTP request payload)
Concurrency Limited by CPU/RAM on your box Handled by the API fleet
Cold start Chromium boot latency One HTTP round-trip

Conclusions

If your Node.js service only needs “JSON in, PDF out”, Puppeteer is usually the wrong tool. Keep the container slim, leave Chromium out of Docker, and let QuartzAPI own layout and pagination.

Ready to remove Puppeteer from production? Create a free QuartzAPI Beta account, design the report template once, and wire pdfService.js into your Express app.

👉 Start free with the QuartzAPI Beta

Ready-to-use snippets

  • Generate: POST https://backend.quartzapi.com/index.php?r=api/v1-jobs/generate-document with templateCode + data.
  • Auth: Authorization: Bearer ${process.env.QUARTZ_API_KEY}.
  • Download: result.downloadUrl or GET …/v1-documents/download&uid=….
  • Docs: Web API documentation.