DEV Community

Zero Lopp Labs
Zero Lopp Labs

Posted on

How I Built a Document Generation API with Bun, Hono & Playwright

Every developer has dealt with PDF generation at some point. Whether it's invoices, contracts, reports, or government forms — the moment a client says "we need this as a PDF," you know you're in for a ride.

I've been through the usual suspects:

  • Puppeteer with headless Chromium — powerful, but managing memory leaks and container sizing in production is a full-time job
  • wkhtmltopdf — reliable for years, but unmaintained and stuck with a 2015-era WebKit engine
  • Low-level libraries like pdfkit or jsPDF — great control, but you're positioning text at pixel coordinates
  • Expensive SaaS solutions — they work, but pricing per page adds up fast

After re-solving this problem on multiple projects, I decided to build it once and expose it as an API. The result is PDFForge — a REST API that handles three document operations:

  • HTML to PDF — design templates in HTML/CSS, render to pixel-perfect PDF
  • Fill PDF forms — send JSON data to fill AcroForm fields
  • Fill Word documents — .docx templates with placeholder tags

The Stack

Layer Choice Why
Runtime Bun Fast startup, native crypto & file I/O, built-in test runner
Framework Hono Lightweight, middleware-friendly, great TypeScript support
PDF Rendering Playwright Chromium-based, reliable CSS rendering
PDF Form Filling pdf-lib Pure JS, no native deps, handles AcroForms well
Word Documents docxtemplater Mature, handles loops/conditionals/images in .docx
Templating Handlebars Simple syntax, custom helpers for formatting
Database Drizzle + PostgreSQL Type-safe ORM, great migration story
Storage S3-compatible (Tigris) Signed URLs for document delivery
Hosting Fly.io Edge deployment, machine suspend for cost control

How It Works

Upload a template

Templates are HTML files with Handlebars syntax. You use variables like clientName, loops with each items, and conditionals. Standard HTML/CSS — no proprietary syntax to learn.

Upload it once via the API or the dashboard.

Generate a document

curl -X POST https://api.pdfforge.dev/v1/documents/generate \
-H "Authorization: Bearer pk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"templateId": "tpl_abc123",
"data": {
"invoiceNumber": "INV-2026-042",
"clientName": "Acme Corp",
"items": [
{"description": "API Integration", "amount": "$2,500"},
{"description": "Support Plan", "amount": "$500"}
],
"total": "$3,000"
}
}'
Enter fullscreen mode Exit fullscreen mode




Get your PDF

The API returns a signed download URL — no auth needed to download, URL expires automatically based on your plan's retention policy.

Interesting Technical Challenges

Chromium Concurrency

The biggest challenge was managing concurrent Playwright instances. Each PDF render spins up a browser context with Chromium, which is memory-hungry. I built a priority semaphore that queues render jobs based on the user's plan tier — paid users get priority over free tier.

Bun + Playwright in Docker

Playwright's bundled Chromium doesn't play well with Bun in Docker. The fix: install Google Chrome Stable separately and point Playwright to it via PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. Counterintuitive, but it works reliably.

Signed URLs vs Streaming

I initially considered streaming PDFs directly in the API response. But signed URLs turned out to be better:

  • Clients can retry failed downloads without re-generating
  • URLs can be shared (temporarily) with end users
  • Decouples generation from delivery

Testing with Bun

Bun's test runner is fast, but mock.module() has a gotcha: if two test files mock the same module differently, the first mock wins globally. The solution was mocking at the lib/ level to isolate dependencies properly.

What's Next

PDFForge is currently in public beta with a free tier (50 documents/month). I'm actively looking for feedback from developers who deal with document generation regularly.

If you've ever struggled with PDF generation in your projects, I'd love to hear about your use case — it helps me prioritize what to build next.

Top comments (0)