Template-Driven Documents vs Custom HTML: When to Use Each

Both approaches generate PDFs, but they solve different problems. Here's a practical breakdown of when templates win, when raw HTML wins, and how to avoid painting yourself into a corner.

pdftemplateshtmldocument-generationapi-design

Template-Driven Documents vs Custom HTML: When to Use Each

If you've ever had to generate PDFs from a backend, you've hit the fork in the road: do you build a templating system where designers (or non-developers) can tweak layouts, or do you just render HTML you fully control in code? Both work. Both have failure modes. Picking the wrong one usually shows up six months later, when you're rewriting the whole pipeline because finance wants a new tax field on the invoice.

This post breaks down the trade-offs and gives you a decision framework.

What "template-driven" actually means

A template-driven setup separates layout from data. You define a document once — usually in something like Handlebars, MJML, or a visual editor — and your backend just sends JSON.

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice-v2", data: { invoiceNumber: "INV-1042", customer: { name: "Acme Co.", email: "[email protected]" }, lineItems: [ { description: "API plan", qty: 1, price: 99 }, ], total: 99, }, });

Your code never touches HTML. The template lives somewhere versioned (a dashboard, a Git repo, a CMS), and changing the design doesn't require a deploy.

What "custom HTML" actually means

Custom HTML means your backend constructs the markup and styles, then hands them to a renderer (Puppeteer, wkhtmltopdf, or an API like Kamy's HTML endpoint).

ts
const html = ` <html> <head><style>${css}</style></head> <body> <h1>Invoice ${invoice.number}</h1> <table> ${invoice.lineItems.map(renderRow).join("")} </table> </body> </html> `; const pdf = await kamy.documents.renderHtml({ html });

You have total control. You also own every pixel, every edge case, and every regression.

When templates win

1. The document layout is stable, but the data isn't. Invoices, receipts, contracts, shipping labels, statements. The shape barely changes month to month, but the values change every request.

2. Non-engineers need to edit it. If your design team, legal team, or a customer wants to adjust wording or branding, you do not want that to be a pull request. Templates make this a config change.

3. You have many tenants with slightly different documents. A SaaS product where each customer wants their logo, accent color, and footer disclaimer is a textbook templating use case. You parameterize a base template instead of forking code paths.

4. You need consistency and auditability. Template versions are easy to pin. If a customer disputes "what did the invoice look like on March 14?", you check which template version was used and re-render with the same data.

ts
await kamy.documents.render({ template: "invoice-v2", templateVersion: "2024-03-01", data: invoiceSnapshot, });

When custom HTML wins

1. Layouts are highly dynamic or programmatically generated. Reports with charts whose structure depends on the data. Dashboards exported as PDFs. Documents where the number of sections, columns, or pages depends on runtime logic that's too gnarly to express in a template language.

2. You're already rendering HTML for the web. If your app shows a beautifully styled report in the browser and you want a PDF that looks identical, just reuse the markup. Don't rebuild it in a template.

ts
const html = await renderReportPage({ reportId }); const pdf = await kamy.documents.renderHtml({ html, options: { format: "A4", printBackground: true }, });

3. One-off or low-volume documents. Building a templating system for a single internal admin export is overkill. Just write the HTML.

4. You need CSS features that templates abstract away. Complex grid layouts, custom fonts loaded from your own CDN, print-specific media queries — these are often easier to express directly.

The hidden trap: don't mix the two badly

The worst pattern is using a template engine to assemble HTML strings, then passing that to a raw HTML renderer. You inherit the downsides of both: untyped string concatenation and no separation of concerns.

If you're going template-driven, commit to it: data in, PDF out, no HTML in your application code. If you're going custom, treat HTML as your interface — render it cleanly, ideally with the same component system your frontend uses.

A practical decision checklist

Ask these in order:

txt
1. Will non-engineers ever need to change this document? → Yes: templates. 2. Does the layout change based on complex runtime logic? → Yes: custom HTML. 3. Do I already have the document rendered in a browser? → Yes: custom HTML (reuse it). 4. Will I have many variants (per-tenant, per-locale)? → Yes: templates. 5. Is this a one-off? → Yes: custom HTML. 6. Otherwise: templates. They scale better long-term.

A hybrid approach

For most products, the right answer is both, used deliberately:

  • Templates for transactional, business-critical documents: invoices, receipts, contracts, statements.
  • Custom HTML for analytical or one-off documents: reports, exports, dashboards.

Kamy supports both in the same SDK, which is the point — you shouldn't need separate infrastructure for "the predictable stuff" and "the weird stuff."

ts
// Transactional: template await kamy.documents.render({ template: "receipt", data }); // Analytical: HTML await kamy.documents.renderHtml({ html: reportHtml });

TL;