PDF generation is a critical feature for SaaS products — invoices, reports, contracts, receipts, certificates. The challenge is rendering complex HTML layouts with proper fonts, pagination, headers, footers, and page breaks into pixel-perfect PDFs. This guide covers every approach: from code-first libraries to browser-based rendering to managed APIs.
PDF Generation Approaches
There are three main categories of PDF generation tools, each with different trade-offs:
- Code-first libraries (PDFKit, jsPDF) — Build PDFs programmatically with coordinates and drawing commands. Full control but tedious for complex layouts.
- Browser-based rendering (Playwright, Puppeteer, wkhtmltopdf) — Render HTML/CSS and print to PDF. Great for complex layouts but requires browser infrastructure.
- Managed APIs (SnapAPI, DocRaptor) — Send HTML or a URL, get a PDF back. No infrastructure, instant results.
Code-First: PDFKit
PDFKit generates PDFs with imperative drawing commands. Best for simple, structured documents where you control every element:
import PDFDocument from 'pdfkit';
import fs from 'fs';
function generateInvoice(invoice) {
const doc = new PDFDocument({ margin: 50 });
doc.pipe(fs.createWriteStream(`invoice-${invoice.number}.pdf`));
// Header
doc.fontSize(20).text('INVOICE', { align: 'right' });
doc.fontSize(10).text(`#${invoice.number}`, { align: 'right' });
doc.moveDown();
// Company info
doc.fontSize(12).text(invoice.company);
doc.fontSize(10)
.text(invoice.address)
.text(invoice.email)
.moveDown(2);
// Bill to
doc.fontSize(12).text('Bill To:');
doc.fontSize(10)
.text(invoice.customer.name)
.text(invoice.customer.email)
.moveDown(2);
// Table header
const tableTop = doc.y;
doc.fontSize(10).font('Helvetica-Bold');
doc.text('Item', 50, tableTop);
doc.text('Qty', 300, tableTop);
doc.text('Price', 370, tableTop);
doc.text('Total', 450, tableTop);
doc.moveTo(50, tableTop + 15)
.lineTo(550, tableTop + 15)
.stroke();
// Table rows
let y = tableTop + 25;
doc.font('Helvetica');
for (const item of invoice.items) {
doc.text(item.name, 50, y);
doc.text(item.qty.toString(), 300, y);
doc.text(`$${item.price.toFixed(2)}`, 370, y);
doc.text(`$${(item.qty * item.price).toFixed(2)}`, 450, y);
y += 20;
}
// Total
doc.moveTo(370, y + 5).lineTo(550, y + 5).stroke();
doc.font('Helvetica-Bold')
.text(`Total: $${invoice.total.toFixed(2)}`, 370, y + 15);
doc.end();
}
PDFKit works but gets verbose fast. Adding a logo, custom fonts, or responsive layouts requires manual positioning for every element. For complex documents, HTML/CSS is much more natural.
Browser-Based: Playwright page.pdf()
Playwright renders any HTML/CSS page and exports it as a PDF with full print stylesheet support:
import { chromium } from 'playwright';
async function htmlToPdf(html, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
await page.pdf({
path: outputPath,
format: 'A4',
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size:9px;width:100%;text-align:center;color:#666;">
Invoice #12345
</div>`,
footerTemplate: `
<div style="font-size:9px;width:100%;text-align:center;color:#666;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
});
await browser.close();
}
// HTML template with print CSS
const invoiceHtml = `
<!DOCTYPE html>
<html>
<head>
<style>
@media print {
body { font-family: 'Helvetica', sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #eee; }
.total-row { font-weight: bold; border-top: 2px solid #333; }
.page-break { page-break-before: always; }
}
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<!-- Full HTML invoice layout -->
</body>
</html>`;
await htmlToPdf(invoiceHtml, 'invoice.pdf');
HTML Templates with Handlebars
For production invoice/report generation, use a template engine to inject dynamic data into HTML templates:
import Handlebars from 'handlebars';
import { chromium } from 'playwright';
import fs from 'fs';
// Register helpers
Handlebars.registerHelper('currency', (val) => `$${val.toFixed(2)}`);
Handlebars.registerHelper('date', (val) =>
new Date(val).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
})
);
const templateSource = fs.readFileSync('invoice-template.html', 'utf8');
const template = Handlebars.compile(templateSource);
async function generateInvoicePdf(invoiceData) {
const html = template(invoiceData);
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
const buffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '2cm', bottom: '2cm', left: '1.5cm', right: '1.5cm' },
});
await browser.close();
return buffer;
}
// Express endpoint
app.post('/api/invoices/:id/pdf', async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
const pdfBuffer = await generateInvoicePdf(invoice);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.number}.pdf"`);
res.send(pdfBuffer);
});
Managed API: SnapAPI /v1/pdf
SnapAPI's PDF endpoint handles all browser rendering, font loading, and PDF generation in a single API call. No Chromium to manage, no template engine to set up — just send a URL or HTML:
import SnapAPI from 'snapapi-js';
const snap = new SnapAPI('sk_live_your_key');
// Generate PDF from a URL
const urlPdf = await snap.pdf({
url: 'https://yourapp.com/invoices/12345',
format: 'A4',
margin: { top: '2cm', bottom: '2cm', left: '1.5cm', right: '1.5cm' },
print_background: true,
display_header_footer: true,
});
console.log(urlPdf.url); // CDN-hosted PDF download URL
// Generate PDF from raw HTML
const htmlPdf = await snap.pdf({
html: '<h1>Invoice #12345</h1><p>Amount: $499.00</p>',
format: 'Letter',
landscape: false,
});
// Use in an Express API
app.get('/api/invoices/:id/pdf', async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
const result = await snap.pdf({
url: `https://yourapp.com/invoices/${invoice.id}?print=true`,
format: 'A4',
print_background: true,
});
res.redirect(result.url); // Redirect to CDN PDF
});
Approach Comparison
| Approach | CSS Support | Fonts | Speed | Infrastructure | Best For |
|---|---|---|---|---|---|
| PDFKit | None (imperative) | Manual embed | Very fast | None | Simple, structured docs |
| jsPDF | None (imperative) | Manual embed | Fast | None | Client-side generation |
| wkhtmltopdf | Partial (WebKit) | System fonts | Fast | Binary install | Simple HTML conversion |
| Playwright/Puppeteer | Full (Chromium) | Full web fonts | Slow (2-5s) | Browser management | Complex layouts |
| SnapAPI /v1/pdf | Full (Chromium) | Full web fonts | Fast (1-3s) | None (managed) | Everything — zero setup |
Generate PDFs from Any URL or HTML — No Setup Required
SnapAPI handles browser rendering, fonts, pagination, headers, and footers. One API call, pixel-perfect PDFs. Free tier includes 200 generations/month.
Start Free — No Credit Card Required