Template-driven documents vs custom HTML: when to use each

Templates give you speed and consistency; raw HTML gives you control. Here's how to decide which approach fits your document generation workflow.

pdftemplateshtmldocument-generationapi-design

Template-driven documents vs custom HTML: when to use each

Every team that generates documents programmatically eventually hits the same fork in the road: do you build on top of pre-made templates with a data payload, or do you ship your own HTML and CSS and render that to PDF? Both approaches work. Both are used in production by serious teams. But they optimize for different things, and picking the wrong one costs you weeks of maintenance pain.

This post walks through the tradeoffs honestly, with examples, so you can make the call for your own stack.

What each approach actually looks like

A template-driven workflow means you (or your designer) define a document layout once — invoice, receipt, contract, report — and then your backend sends structured data to render it. The call looks like this:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "invoice", data: { invoiceNumber: "INV-2024-0421", customer: { name: "Acme Corp", email: "[email protected]" }, lineItems: [ { description: "Pro plan", quantity: 1, unitPrice: 4900 }, ], total: 4900, }, });

Your backend never touches HTML. It just builds a JSON payload.

A custom HTML workflow means you render HTML yourself — usually with a server-side template engine like Handlebars, JSX, or Jinja — and hand that string to a PDF renderer:

ts
const html = await renderInvoiceHTML(order); const pdf = await kamy.documents.renderHtml({ html, options: { format: "A4", margin: "20mm" }, });

You own the markup, the styles, the fonts, the page breaks, everything.

When templates win

Templates are the right call when the document is structured, repetitive, and owned by someone other than the engineer. That covers most business documents: invoices, quotes, order confirmations, shipping labels, policy certificates, payslips.

The real wins:

  • Non-engineers can edit the layout. Finance wants a new tax line? Legal needs new disclaimer text? They don't open a PR.
  • Your backend stays small. No HTML, no CSS, no font loading, no wkhtmltopdf-in-a-docker-container headache. Just JSON.
  • Versioning is clean. Template v3 renders old invoices identically forever. You deploy backend changes without worrying about layout regressions.
  • Localization is free. The template system handles locale-specific formatting; your payload stays the same.

A typical server call ends up being this short:

ts
app.post("/invoices/:id/pdf", async (req, res) => { const invoice = await db.invoice.findUnique({ where: { id: req.params.id } }); const { url } = await kamy.documents.render({ template: "invoice-v3", data: invoice, }); res.json({ url }); });

That's the whole handler. No markup, no styling logic, no page-break hacks.

When custom HTML wins

Templates break down when the document is irregular, highly dynamic, or the layout itself is the product. Think:

  • Generated reports where the structure depends on query results (variable sections, dynamic charts, pivoted tables).
  • Marketing collateral or proposals where every document is bespoke.
  • Documents with heavy conditional logic — "if customer is in EU and plan is enterprise and..." branching through five sections.
  • Anything where your frontend already renders the exact view and you just want a PDF of it.

In those cases, HTML gives you the full web platform: flexbox, grid, SVG, custom fonts, CSS variables, print media queries. You can reuse the same React or Vue component you use in the app.

A React-to-PDF flow often looks like this:

ts
import { renderToString } from "react-dom/server"; import { ReportDocument } from "./ReportDocument"; const html = `<!doctype html><html><head> <link rel="stylesheet" href="https://cdn.example.com/report.css"> </head><body>${renderToString(<ReportDocument data={report} />)}</body></html>`; const pdf = await kamy.documents.renderHtml({ html });

The downside: you now own print styling. @page, page-break-inside: avoid, running headers, footnotes — none of that is free, and getting a complex report to paginate cleanly is genuinely harder than it sounds.

A quick decision checklist

Use templates if you can answer "yes" to most of these:

  • The document has a stable, well-known structure.
  • Non-engineers will want to tweak copy or layout.
  • You're generating thousands or millions of them.
  • You don't want HTML/CSS in your service.

Use custom HTML if you can answer "yes" to most of these:

  • Each document is substantially different.
  • The layout depends on runtime data in non-trivial ways.
  • You already have a web view that matches what you want in the PDF.
  • You need pixel-level control over typography or layout.

You don't have to pick one

In practice, most teams use both. Invoices and receipts go through templates. The occasional one-off — a custom quote, an annual report, a certificate — gets rendered from HTML. A good PDF API should handle both without forcing you into a single model. Kamy exposes documents.render for templated flows and documents.renderHtml for raw HTML, so you can mix them per endpoint.

The failure mode to avoid is forcing a template system to do what HTML does well (layering ten conditional blocks into an "invoice" template until it's unrecognizable), or forcing HTML to do what templates do well (asking your backend team to redeploy every time finance changes a label).

Pick the tool that matches the document. If the document type grows past the tool's sweet spot, migrate it. That's a much cheaper refactor than you'd expect — the data model usually