How to convert HTML to PDF in Node.js
Two approaches that work in production, with the code for each: run headless Chrome yourself with Puppeteer, or send the HTML to an API and get the PDF back.
By Saurav N, senior software engineer · Updated
· 6 min readOption 1: Puppeteer and headless Chrome
Puppeteer drives a real Chrome, so the PDF looks like the page does in the browser: modern CSS, web fonts and @page rules all work. page.pdf() prints the page the way Chrome’s print dialog would.
Set printBackground: true, or background colours and table stripes disappear. waitUntil: networkidle0 waits for images and fonts before printing.
import puppeteer from 'puppeteer';
import { writeFile } from 'node:fs/promises';
const html = '<h1>Invoice INV-2026-0142</h1><p>Total ₹49,402.50</p>';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true, margin: { top: '16mm', bottom: '16mm' } });
await writeFile('invoice.pdf', pdf);
} finally {
await browser.close();
}What running Chrome yourself costs
It works well, and it is the right call for many teams. The work is around it:
- Each Chrome process takes a few hundred megabytes of memory; under load you pool browsers or pages and restart the ones that leak.
- Server images need Chrome and its system libraries, plus the fonts your documents use (including Indic scripts, if you print them).
- Rendering on the request path makes slow PDFs slow responses; most teams move it to a queue.
Option 2: one call to an HTML to PDF API
An API does the Chrome part for you. With Templater the HTML lives in a template your team edits in the browser, and your code sends only the data. ?wait=20 holds the request until the PDF is ready, and returnAs: "binary" returns the file itself.
import { writeFile } from 'node:fs/promises';
const res = await fetch('https://templater.nestutils.com/v1/pdfs/render?wait=20', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.TEMPLATER_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
template: 'gst-invoice',
returnAs: 'binary',
data: { invoice: { number: 'INV-2026-0142', total: 49402.5 }, customer: { name: 'Priya Menon' } },
}),
});
if (res.status === 202) {
// Still rendering after 20 s: poll GET /v1/pdfs/:id with the id in the JSON body, or use a webhook.
const { data } = await res.json();
console.log('queued', data.id);
} else if (res.ok) {
await writeFile('invoice.pdf', Buffer.from(await res.arrayBuffer()));
} else {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}Which one should you use?
Use Puppeteer when you already run containers with Chrome in them, render a handful of document types, and developers own every change to the HTML.
Use an API when: You want the same template edited by your team in a browser, previewed on real data, versioned, and rendered without running Chrome yourself. You pay per render instead of per server.
Questions
Does Puppeteer work on serverless platforms?
Yes, with a Chromium build made for it (such as @sparticuz/chromium on AWS Lambda) and enough memory. Cold starts add a few seconds to the first render.
Why is my PDF missing background colours?
Chrome drops backgrounds when printing unless you pass printBackground: true, or set print-color-adjust: exact in your CSS.
Can I add page numbers?
Yes. With Puppeteer use displayHeaderFooter with a footerTemplate containing <span class="pageNumber">. In a Templater PDF template, put {{pageNumber}} and {{totalPages}} in the footer.