PDF Generation: API vs Library — How to Choose in 2026
Every web application eventually needs to generate PDFs. Invoices, reports, contracts, certificates — the list goes on. When that moment arrives, you face a fundamental choice:
- Library approach — install a PDF generation library on your own server
- API approach — send HTTP requests to a cloud PDF generation service
Both approaches have genuine strengths. The right choice depends on your project's specific requirements. This guide breaks down each approach in detail so you can make a confident decision.
The Library Approach
PDF generation libraries have been around for decades. They run on your infrastructure and give you direct control over the rendering process.
Popular PDF Libraries
| Library | Language | Approach |
|---|---|---|
| Puppeteer | Node.js | Controls headless Chrome to convert HTML to PDF |
| wkhtmltopdf | CLI | QtWebKit-based conversion tool (archived 2023) |
| pdfkit | Node.js | Programmatic PDF construction |
| jsPDF | JavaScript | Browser/Node.js PDF generation |
| TCPDF | PHP | Long-standing PHP PDF library |
| ReportLab | Python | Python PDF generation (commercial license available) |
| Prawn | Ruby | Ruby PDF generation |
Pros of Libraries
Full control over the process
Everything runs on your servers. You decide which fonts are installed, how rendering is configured, and what resources are allocated. For edge cases that require low-level PDF manipulation, a library gives you direct access.
No external dependencies
No network calls means no reliance on third-party uptime. Your PDF generation works offline and is unaffected by external service outages.
Predictable costs at low volume
There are no per-PDF charges. If you generate a handful of PDFs per month, running a library on an existing server costs essentially nothing extra.
Cons of Libraries
Setup and maintenance overhead
Taking Puppeteer as an example, you need to handle Chrome installation, font provisioning, memory management, zombie process cleanup, and Docker configuration:
// Puppeteer invoice generation
const puppeteer = require('puppeteer');
async function generateInvoice(html) {
// Launches a Chrome process (~100MB+ memory each)
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage', // Required in Docker
'--font-render-hinting=none',
],
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
});
return pdf;
} finally {
await browser.close(); // Must clean up or memory leaks
}
}
Font and rendering issues
CJK (Chinese, Japanese, Korean) text is a notorious pain point. Linux servers typically lack CJK fonts, resulting in "tofu" characters (empty rectangles). Adding font packages bloats Docker images significantly:
# Font provisioning in Docker (+200MB or more)
FROM node:20-slim
RUN apt-get update && apt-get install -y \
fonts-noto-cjk \
fonts-noto-cjk-extra \
&& rm -rf /var/lib/apt/lists/*
Even with fonts installed, rendering differences between environments (macOS vs Linux, different Chrome versions) can produce subtly different PDFs.
Scaling is your problem
Headless Chrome consumes over 100MB per process. Ten concurrent requests need 1GB+ of memory. You must build your own queuing, pool management, and autoscaling infrastructure. This is solvable, but it is engineering effort that does not directly add product value.
The API Approach
A PDF generation API accepts HTML (or a template) via HTTP and returns a PDF binary. The rendering infrastructure is fully managed by the API provider.
Pros of APIs
Near-zero setup time
Get an API key, send an HTTP request, receive a PDF. You can go from zero to working PDF generation in minutes.
# Generate a PDF with a single curl command (FUNBREW PDF)
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Invoice</h1><p>Total: $1,000.00</p>",
"options": {
"format": "A4",
"margin": { "top": "20mm", "bottom": "20mm" }
}
}' \
--output invoice.pdf
Consistent rendering
The API provider optimizes the browser engine, fonts, and rendering configuration. You get the same output every time, regardless of your local environment. APIs like FUNBREW PDF ship with CJK fonts built in, eliminating the tofu problem entirely.
Built-in scaling
Whether you generate 100 PDFs a month or 1 million, the same API call works. Scaling is the provider's responsibility, not yours.
Language-agnostic
A REST API works from any programming language. Node.js, Python, PHP, Ruby, Go — your entire team uses the same PDF generation endpoint. This is especially valuable for organizations running multiple services in different languages. Check the quickstart guide for setup instructions in each language.
Cons of APIs
Per-PDF costs
API usage incurs charges. However, when you factor in the server resources and engineering time required by the library approach, the total cost of ownership often favors APIs — especially at scale. See the pricing comparison for details.
Network dependency
HTTP requests are required, so fully offline environments cannot use an API. Network latency is added to each request, though rendering-optimized APIs typically respond fast enough for synchronous use.
Data privacy considerations
Your HTML content is sent to an external server. For highly sensitive data, verify that the API provider's data handling meets your compliance requirements.
Comparison Table
| Criteria | Library | API |
|---|---|---|
| Setup time | Hours to days | Minutes |
| Rendering quality | Environment-dependent | Consistent |
| CJK / font support | Manual font provisioning | Built-in (varies by provider) |
| Scaling | Self-managed | Automatic |
| Cost model | Server costs | Per-PDF pricing |
| Maintenance burden | High (OS, library updates) | Low |
| Offline support | Yes | No |
| Language support | Library-specific | Any language |
Decision Framework
Choose a library when:
- Simple PDFs: Text and tables only, minimal styling
- No CJK required: English-only documents
- Low volume: A few dozen PDFs per month
- Strict data locality: Regulations prohibit sending content to external services
- Existing infrastructure: You already have a stable Puppeteer/wkhtmltopdf setup
Choose an API when:
- Complex layouts: CSS Grid, Flexbox, background images, modern CSS features
- CJK text: Japanese, Chinese, or Korean content
- High volume: Thousands to millions of PDFs per month
- Cross-language teams: Multiple services in different languages need PDF generation
- Speed to market: You need PDF output working today, not next sprint
Migration Path: Library to API
If you already have a Puppeteer-based setup, migration is straightforward. Your HTML templates stay exactly the same — you only replace the "HTML to PDF" conversion step.
// Before: Puppeteer (30+ lines of setup code)
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ /* config... */ });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();
// After: API call (5 lines)
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, options: { format: 'A4' } }),
});
const pdf = await response.arrayBuffer();
For a detailed step-by-step walkthrough, see the Puppeteer to PDF API migration guide.
Code Comparison: Same Invoice, Both Approaches
Let's generate the same invoice using both approaches to see the practical difference.
Puppeteer (Library)
const puppeteer = require('puppeteer');
async function generateInvoicePdf(invoiceData) {
const html = buildInvoiceHtml(invoiceData);
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdfBuffer = await page.pdf({
format: 'A4',
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate:
'<div style="font-size:8px;text-align:center;width:100%;">Page <span class="pageNumber"></span></div>',
});
await browser.close();
return pdfBuffer;
}
FUNBREW PDF API
async function generateInvoicePdf(invoiceData) {
const html = buildInvoiceHtml(invoiceData); // Same HTML template
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
printBackground: true,
},
}),
});
return await response.arrayBuffer();
}
The buildInvoiceHtml() function is identical in both cases. You can reuse all your existing HTML/CSS templates. Try it yourself in the Playground to see the output.
Python: ReportLab vs API
# ReportLab — build PDF programmatically with coordinates
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
def generate_invoice(data, output_path):
c = canvas.Canvas(output_path, pagesize=A4)
c.setFont("Helvetica-Bold", 20)
c.drawString(50, 780, "INVOICE")
c.setFont("Helvetica", 12)
c.drawString(50, 750, f"Invoice #: {data['number']}")
c.drawString(50, 730, f"Date: {data['date']}")
# ... manually position every element with coordinates
c.save()
# FUNBREW PDF API — use HTML templates
import requests
def generate_invoice(data):
html = render_template("invoice.html", data=data)
response = requests.post(
"https://pdf.funbrew.cloud/api/v1/generate",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"html": html, "options": {"format": "A4"}},
)
return response.content
ReportLab uses coordinate-based positioning, meaning every layout change requires recalculating positions manually. With an HTML/CSS-based API, design changes are just CSS edits. For CSS tips specific to PDF output, see HTML to PDF CSS techniques.
Summary
Neither approach is universally better. The right choice depends on your context:
- Small scale, English-only, existing setup — keep your library
- CJK support, scalability, development speed — an API is the stronger choice
If you are starting a new project, consider beginning with an API to ship quickly. You can always move to a self-hosted library later if your requirements demand it.
FUNBREW PDF offers a free tier to get started. Try the Playground to test your HTML templates interactively, or follow the complete HTML to PDF guide for a comprehensive walkthrough.
Related Articles
- HTML to PDF API Comparison 2026 — comparing API providers against each other
- Puppeteer to PDF API Migration Guide — step-by-step migration
- HTML to PDF Complete Guide — comprehensive overview
- PDF API Production Guide — best practices for production use