April 13, 2026

PDF Generation: API vs Library — How to Choose in 2026

PDF generationAPIlibrarycomparison

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:

  1. Library approach — install a PDF generation library on your own server
  2. 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

Powered by FUNBREW PDF