PDF generation is one of those features that seems simple until you actually build it. Invoices, reports, contracts, receipts, documentation exports — every SaaS eventually needs to generate PDFs from dynamic content. The challenge is that HTML rendering in PDF context is full of edge cases: page breaks, headers/footers, font embedding, background graphics, and CSS print media queries.
This guide covers every approach to HTML-to-PDF conversion in 2026: from low-level libraries to browser-based rendering to API services that handle everything for you.
Three Approaches to PDF Generation
There are fundamentally three ways to generate PDFs from HTML:
- Library-based: PDFKit, jsPDF, pdfmake — you construct the PDF programmatically. Full control but no HTML rendering.
- Browser-based: Playwright/Puppeteer
page.pdf()— renders HTML in a real browser, then prints to PDF. Best fidelity but requires browser infrastructure. - API-based: Send a URL or HTML string, get back a PDF. No infrastructure to manage.
Library Approach — PDFKit
PDFKit gives you low-level control over every element in the PDF. Great for structured documents like invoices where you control the exact layout:
import PDFDocument from 'pdfkit';
import fs from 'fs';
function generateInvoice(data) {
const doc = new PDFDocument({ margin: 50 });
const stream = fs.createWriteStream(`invoice-${data.number}.pdf`);
doc.pipe(stream);
// Header
doc.fontSize(20).text('INVOICE', 50, 50);
doc.fontSize(10)
.text(`Invoice #: ${data.number}`, 50, 80)
.text(`Date: ${data.date}`, 50, 95)
.text(`Due: ${data.due}`, 50, 110);
// Customer info
doc.fontSize(12).text('Bill To:', 50, 150);
doc.fontSize(10)
.text(data.customer.name, 50, 170)
.text(data.customer.address, 50, 185)
.text(data.customer.email, 50, 200);
// Line items table
let y = 250;
doc.fontSize(10).font('Helvetica-Bold');
doc.text('Item', 50, y);
doc.text('Qty', 300, y);
doc.text('Price', 370, y);
doc.text('Total', 450, y);
doc.moveTo(50, y + 15).lineTo(550, y + 15).stroke();
y += 25;
doc.font('Helvetica');
for (const item of data.items) {
doc.text(item.description, 50, y);
doc.text(item.quantity.toString(), 300, y);
doc.text(`$${item.price.toFixed(2)}`, 370, y);
doc.text(`$${(item.quantity * item.price).toFixed(2)}`, 450, y);
y += 20;
}
// Total
doc.moveTo(350, y + 5).lineTo(550, y + 5).stroke();
y += 15;
doc.font('Helvetica-Bold')
.text('Total:', 370, y)
.text(`$${data.total.toFixed(2)}`, 450, y);
doc.end();
return new Promise(resolve => stream.on('finish', resolve));
}
Browser Approach — Playwright page.pdf()
Playwright's page.pdf() renders HTML in a real Chromium browser and exports to PDF. This gives you pixel-perfect rendering of any HTML/CSS:
import { chromium } from 'playwright';
async function htmlToPdf(options: {
url?: string;
html?: string;
format?: string;
margin?: { top: string; bottom: string; left: string; right: string };
printBackground?: boolean;
headerTemplate?: string;
footerTemplate?: string;
}): Promise<Buffer> {
const browser = await chromium.launch();
const page = await browser.newPage();
if (options.url) {
await page.goto(options.url, { waitUntil: 'networkidle' });
} else if (options.html) {
await page.setContent(options.html, { waitUntil: 'networkidle' });
}
const pdf = await page.pdf({
format: options.format ?? 'A4',
printBackground: options.printBackground ?? true,
margin: options.margin ?? {
top: '1cm', bottom: '1cm',
left: '1cm', right: '1cm',
},
displayHeaderFooter: !!(options.headerTemplate || options.footerTemplate),
headerTemplate: options.headerTemplate ?? '',
footerTemplate: options.footerTemplate ??
'<div style="font-size:8px;text-align:center;width:100%;">' +
'Page <span class="pageNumber"></span> of ' +
'<span class="totalPages"></span></div>',
});
await browser.close();
return pdf;
}
Print CSS for Clean PDFs
@media print {
/* Hide non-printable elements */
nav, footer, .sidebar, .cookie-banner { display: none !important; }
/* Control page breaks */
h2, h3 { page-break-after: avoid; }
table, figure { page-break-inside: avoid; }
.page-break { page-break-before: always; }
/* Ensure backgrounds print */
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
/* Readable link URLs */
a[href]::after { content: " (" attr(href) ")"; font-size: 0.8em; color: #666; }
}
print-color-adjust: exact to preserve background colors and images in PDFs. Without it, browsers strip backgrounds by default for print.
API Approach — One Request, Professional PDF
A PDF generation API handles browser management, font rendering, and all the edge cases. Send a URL or HTML, get back a PDF:
Generate PDF from URL
const response = await fetch('https://api.snapapi.pics/v1/pdf', {
method: 'POST',
headers: {
'X-Api-Key': 'sk_live_your_key_here',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://myapp.com/invoice/12345',
format: 'A4',
print_background: true,
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
}),
});
const pdfBuffer = await response.arrayBuffer();
// Save or email the PDF
fs.writeFileSync('invoice.pdf', Buffer.from(pdfBuffer));
Generate PDF from Raw HTML
const invoiceHtml = `
<html>
<head>
<style>
body { font-family: 'Helvetica', sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.logo { font-size: 24px; font-weight: bold; color: #6366f1; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8fafc; font-weight: 600; }
.total { font-size: 18px; font-weight: bold; text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<div class="logo">Acme Corp</div>
<div>Invoice #INV-2026-001<br>Date: April 5, 2026</div>
</div>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
<tr><td>API Pro Plan</td><td>1</td><td>$79.00</td><td>$79.00</td></tr>
<tr><td>Extra requests (10K)</td><td>2</td><td>$15.00</td><td>$30.00</td></tr>
</table>
<div class="total">Total: $109.00</div>
</body>
</html>`;
const response = await fetch('https://api.snapapi.pics/v1/pdf', {
method: 'POST',
headers: {
'X-Api-Key': 'sk_live_your_key_here',
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: invoiceHtml,
format: 'A4',
print_background: true,
}),
});
Common PDF Generation Use Cases
Invoice Generation
Generate invoices from order data by rendering an HTML template and converting to PDF. Use URL mode if you have an invoice page, or HTML mode to render templates server-side with Handlebars, EJS, or React.
Report Export
Let users download dashboard data as PDF reports. Screenshot the dashboard at full page, or build a print-optimized template that includes charts, tables, and summaries.
Contract & Legal Documents
Generate contracts from templates with variable data (names, dates, amounts). Use print CSS to control page breaks and ensure legal formatting requirements.
Receipt Generation
Generate receipts after purchases or donations. Use HTML mode with a receipt template — faster than URL mode since there's no page to load.
Documentation Export
Let users download documentation as PDF. Capture the docs page with full-page rendering and proper print styles for clean output.
Python Example
import requests
# Generate PDF from URL
response = requests.post(
'https://api.snapapi.pics/v1/pdf',
headers={'X-Api-Key': 'sk_live_your_key_here'},
json={
'url': 'https://myapp.com/report/monthly',
'format': 'A4',
'print_background': True,
'margin': {'top': '2cm', 'bottom': '2cm'},
}
)
with open('report.pdf', 'wb') as f:
f.write(response.content)
print(f"PDF generated: {len(response.content)} bytes")
Approach Comparison
| Factor | PDFKit (Library) | Playwright (Browser) | SnapAPI (API) |
|---|---|---|---|
| HTML rendering | ❌ No HTML support | ✅ Full browser rendering | ✅ Full browser rendering |
| CSS support | ❌ Manual styling | ✅ Full CSS + print media | ✅ Full CSS + print media |
| Infrastructure | None (npm package) | Chromium binary required | None (API call) |
| Page headers/footers | Manual positioning | ✅ Template-based | ✅ Template-based |
| Web fonts | Manual embedding | Requires font installation | ✅ Automatic |
| JavaScript rendering | ❌ | ✅ | ✅ |
| URL to PDF | ❌ | ✅ With navigation code | ✅ Single parameter |
| Best for | Simple, structured docs | Full control, self-hosted | Speed, scale, no infra |
Getting Started with SnapAPI PDF Generation
- Sign up free at snapapi.pics — 200 requests/month included
- Get your API key from the dashboard
- Generate your first PDF — pass a URL or raw HTML
- Install an SDK — JavaScript, Python, Go, PHP, Swift, Kotlin, and more
Generate PDFs Without Infrastructure
URL to PDF. HTML to PDF. Custom margins, headers, footers. 200 free requests/month. No credit card required.
Get Your Free API Key