---
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.
