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"
}
}'
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)