146 lines
5.5 KiB
Markdown
146 lines
5.5 KiB
Markdown
---
|
|
title: "I Built an HTML-to-PDF API in a Day — Here's How It Works"
|
|
published: false
|
|
tags: node, javascript, api, pdf
|
|
---
|
|
|
|
Every developer has that one problem they keep solving over and over. For me, it's PDF generation.
|
|
|
|
Invoice for a freelance project? PDF. Report for a dashboard? PDF. Receipt for an e-commerce app? PDF. And every time, I'd end up in the same rabbit hole: installing wkhtmltopdf, fighting with Docker images, or configuring Puppeteer from scratch.
|
|
|
|
So I built [DocFast](https://docfast.dev) — a simple API that takes HTML or Markdown and returns a PDF. Here's how it works under the hood.
|
|
|
|
## The Architecture
|
|
|
|
DocFast is straightforward:
|
|
|
|
- **Express** handles the API layer
|
|
- **Puppeteer** (headless Chromium) does the actual rendering
|
|
- A **pool of browser instances** keeps things fast
|
|
|
|
The core flow is:
|
|
|
|
1. You POST HTML/Markdown + options to the API
|
|
2. The server loads it into a pooled Chromium page
|
|
3. Chromium renders it and exports to PDF
|
|
4. You get the PDF back
|
|
|
|
That's it. No queue, no webhook callback, no async polling. Request in, PDF out.
|
|
|
|
## Why Puppeteer Over wkhtmltopdf?
|
|
|
|
I tried both. wkhtmltopdf is fast but uses an ancient WebKit engine — modern CSS (flexbox, grid, custom properties) breaks in weird ways. Puppeteer uses actual Chromium, so if it looks right in Chrome, it looks right in the PDF.
|
|
|
|
The tradeoff is resource usage. Chromium is heavy. That's where the browser pool comes in — instead of launching a new browser per request, I maintain a pool of warm instances. A new page in an existing browser takes ~50ms vs ~2s for a cold start.
|
|
|
|
## API Design Choices
|
|
|
|
I wanted the API to feel obvious. If you've used any REST API, you already know how to use DocFast.
|
|
|
|
### Basic HTML to PDF
|
|
|
|
```bash
|
|
curl -X POST https://docfast.dev/v1/convert/html \
|
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"html": "<h1>Invoice #1042</h1><p>Amount: $250.00</p>"}' \
|
|
--output invoice.pdf
|
|
```
|
|
|
|
### Markdown to PDF
|
|
|
|
```bash
|
|
curl -X POST https://docfast.dev/v1/convert/html \
|
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"markdown": "# Monthly Report\n\n| Metric | Value |\n|--------|-------|\n| Users | 1,204 |"}' \
|
|
--output report.pdf
|
|
```
|
|
|
|
### With Options
|
|
|
|
```javascript
|
|
const response = await fetch('https://docfast.dev/v1/convert/html', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_API_KEY',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
html: '<h1>Styled Doc</h1>',
|
|
css: 'h1 { color: navy; font-family: Georgia; }',
|
|
options: {
|
|
format: 'A4',
|
|
margin: { top: '20mm', bottom: '20mm' },
|
|
header: '<div style="font-size:10px;text-align:right;">Page <span class="pageNumber"></span></div>',
|
|
footer: '<div style="font-size:10px;text-align:center;">Confidential</div>'
|
|
}
|
|
})
|
|
});
|
|
|
|
const pdf = await response.blob();
|
|
```
|
|
|
|
A few design decisions I'm happy with:
|
|
|
|
- **HTML and Markdown in the same endpoint.** Send `html` or `markdown` — the API figures it out. No separate routes.
|
|
- **CSS as a separate field.** Keeps your HTML clean. You can also inline it, obviously.
|
|
- **Sensible defaults.** If you send just `{"html": "..."}`, you get an A4 PDF with reasonable margins. No required options.
|
|
|
|
## Built-in Templates
|
|
|
|
This is the part I'm most excited about. DocFast ships with templates for common document types:
|
|
|
|
```javascript
|
|
const response = await fetch('https://docfast.dev/v1/convert/html', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_API_KEY',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
template: 'invoice',
|
|
data: {
|
|
company: 'Acme Inc.',
|
|
items: [
|
|
{ description: 'Web Development', quantity: 40, rate: 150 },
|
|
{ description: 'Design Review', quantity: 5, rate: 150 }
|
|
],
|
|
due_date: '2026-03-01'
|
|
}
|
|
})
|
|
});
|
|
```
|
|
|
|
You pass structured data, the template handles layout and styling. No HTML authoring needed for standard documents. Currently there are templates for invoices, receipts, and reports — more coming based on what people actually need.
|
|
|
|
## Performance
|
|
|
|
Some rough numbers from production:
|
|
|
|
- Simple HTML (text, basic styling): **~200ms**
|
|
- Complex HTML (tables, images, custom fonts): **~800ms**
|
|
- Template with data injection: **~400ms**
|
|
|
|
Most of the time is Chromium rendering. The API overhead is minimal. For comparison, spinning up Puppeteer yourself (cold start in a Lambda/Cloud Function) typically takes 3-8 seconds.
|
|
|
|
## Pricing and Why There's a Free Tier
|
|
|
|
Free tier: **100 PDFs/month**, no credit card required. Enough for side projects, prototyping, or low-volume internal tools.
|
|
|
|
Pro: **$9/month for 10,000 PDFs**. That's $0.0009 per PDF.
|
|
|
|
I wanted the free tier to be actually usable, not a glorified trial. If you're generating fewer than 100 PDFs a month, you never need to pay. If your project takes off and you need more, $9/mo is less than most developers spend on coffee in a week.
|
|
|
|
## What I'd Do Differently
|
|
|
|
If I rebuilt this from scratch:
|
|
|
|
- **I'd start with templates earlier.** The raw HTML→PDF conversion was the obvious MVP, but templates are what people actually get excited about.
|
|
- **I'd add a preview endpoint sooner.** Being able to see the PDF in-browser before committing to generation saves a lot of back-and-forth.
|
|
|
|
## Try It
|
|
|
|
The docs are at [docfast.dev/docs](https://docfast.dev/docs). You can grab an API key in about 30 seconds.
|
|
|
|
If you have feedback on the API design, templates you'd want, or bugs — I'm all ears. Building this solo, so every piece of feedback actually gets read.
|