The two Node.js routes: Handlebars with Puppeteer, or one fetch call
To generate a PDF from a template in Node.js, compile an HTML template with Handlebars, load the result into headless Chrome with Puppeteer and call page.pdf(). Everything involved is open source. The alternative is a template API: the layout lives in a hosted editor and your code sends JSON with fetch.
Node.js has a third option for layouts drawn in code, PDFKit, which its own site describes as a PDF generation library for Node and the browser. It suits simple documents drawn in code; there is no HTML template to hand to a designer.
Route 1: Handlebars and Puppeteer's page.pdf, step by step
Prerequisites. A Node.js project and somewhere that can run Chrome. Installing Puppeteer downloads a Chrome for Testing build of about 170 MB on macOS, 282 MB on Linux and 280 MB on Windows, according to its installation guide. That matters where deployment size is limited.
- Install Puppeteer:
npm i puppeteer. Add the Handlebars package from npm as well. - Compile the template source:
const template = Handlebars.compile(source); - Merge the data:
const html = template(data); - Start the browser and open a page:
const browser = await puppeteer.launch();thenconst page = await browser.newPage(); - Load the HTML:
await page.setContent(html); - Write the PDF:
await page.pdf({ path: 'invoice.pdf', printBackground: true }); - Shut down:
await browser.close();
Leave out path and page.pdf() still resolves with the PDF as a Uint8Array, ready to send in an HTTP response. Options documented in PDFOptions include format (default letter), landscape, margin, displayHeaderFooter with headerTemplate and footerTemplate, and preferCSSPageSize. Header and footer templates can use the classes pageNumber and totalPages.
Route 2: a template API called with fetch
Prerequisites. A CraftMyPDF account, a template built in its editor, the template ID and an API key. The Free plan allows 50 PDFs a month and 3 templates (pricing page, 3 October 2026). No browser is needed on your side.
- Set the headers:
const headers = { "Content-Type": "application/json", "X-API-KEY": apiKey }; - Set the body:
const body = JSON.stringify({ template_id: "05f77b2b18ad809a", export_type: "json", data }); - Send the request:
const res = await fetch("https://api.craftmypdf.com/v1/create", { method: "POST", headers, body }); - Read the link:
const { file } = await res.json();
file is the URL of the generated PDF. With export_type set to file the response is the PDF itself and nothing is stored. For volume, the reference describes /v1/create-async, which queues the job and calls your webhook_url when the file is ready. It also states rate limits of 100 requests per 10 seconds per IP address. The CraftMyPDF API guide has the details.
Worked example: a one-page invoice with a colored header
The template invoice.hbs holds {{invoice_number}}, {{date}} and {{total_amount}} inside a table, with a header row styled by a CSS background color. The data object is { invoice_number: "INV38379", date: "2021-09-30", total_amount: 82542.56 }.
On route 1, steps 2 to 6 produce invoice.pdf on disk in letter size. On route 2 the same object is sent as data, and you get back a link that expires after 5 minutes unless you pass expiration with a longer value in minutes.
| Handlebars + Puppeteer | Template API (CraftMyPDF) | |
|---|---|---|
| Layout is edited in | An HTML and CSS file | A drag-and-drop editor |
| You must run | Headless Chrome | Nothing |
| Cost | Open source, plus your server | Free for 50 PDFs a month, then from $29 a month |
| Good fit | Developer-owned layouts, existing HTML | Layouts changed by non-developers, serverless hosts |
A mistake to avoid: backgrounds and print styles go missing
The PDF from page.pdf() can look different from the page on screen, and the documentation explains why. The method renders with the print CSS media type, and printBackground defaults to false, so background colors and images are dropped. Set printBackground: true, and if you want screen styles call page.emulateMediaType('screen') first. Colors may also be adjusted for print unless your CSS sets -webkit-print-color-adjust.
Two smaller traps. Handlebars escapes values inside {{expression}}, so HTML in your data shows as text unless you use the triple-stash form. And tables that span pages need explicit CSS; see the page breaks guide.
If your team is not sure it wants to own a browser and templates at all, no-code or API compares the lighter options, and the HTML to PDF how-to covers the same Puppeteer calls without a template engine.
Sources used for this page
The facts above come from the pages below, read on 3 October 2026. We have not used these products hands-on. Prices and terms change, so confirm them on the vendor's site before you buy.
- Installation | Puppeteer — Vendor documentation · pptr.dev · checked 2026-10-03
- PDF generation | Puppeteer — Vendor documentation · pptr.dev · checked 2026-10-03
- Page.setContent() method | Puppeteer — Vendor documentation · pptr.dev · checked 2026-10-03
- Page.pdf() method | Puppeteer — Vendor documentation · pptr.dev · checked 2026-10-03
- PDFOptions interface | Puppeteer — Vendor documentation · pptr.dev · checked 2026-10-03
- Handlebars API reference: compilation — Vendor documentation · handlebarsjs.com · checked 2026-10-03
- Handlebars guide: introduction and HTML escaping — Vendor documentation · handlebarsjs.com · checked 2026-10-03
- PDFKit: a JavaScript PDF generation library — Vendor documentation · pdfkit.org · checked 2026-10-03
- PDF Generation API - CraftMyPDF — Vendor product page · craftmypdf.com · checked 2026-10-03
- CraftMyPDF API Reference (OpenAPI specification) — Vendor documentation · craftmypdf.com · checked 2026-10-03
- Pricing - CraftMyPDF.com — Vendor pricing page · craftmypdf.com · checked 2026-10-03