HTML to PDF
in Node.js.
Node has had the same two options for years: ship headless Chrome with your app, or draw the PDF by hand. The third is to post the HTML somewhere that already runs Chrome and get the bytes back.
The call
Post your markup with an API key. The PDF comes back in the same response, base64 encoded by default. Nothing is written to disk on our side.
import { writeFile } from "node:fs/promises"
const res = await fetch("https://api.html4pdf.com/generate-pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTML4PDF_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html: "<h1>Invoice INV-1042</h1><p>Total: $1,645.40</p>",
options: { format: "A4", printBackground: true },
}),
})
if (!res.ok) throw new Error(`html4pdf ${res.status}: ${await res.text()}`)
const { pdf, creditsRemaining } = await res.json()
await writeFile("invoice.pdf", Buffer.from(pdf, "base64"))
console.log("saved invoice.pdf,", creditsRemaining, "credits left")Skip the base64 step
Set responseFormat to binary and the response body is the PDF itself. You lose the credit count in the JSON, but you save a third of the transfer and a decode.
const res = await fetch("https://api.html4pdf.com/generate-pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTML4PDF_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, responseFormat: "binary" }),
})
await writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()))What comes back
The default response is JSON with the document base64 encoded, the credits you have left, and how long the render took. Send responseFormat as binary instead and the body is the PDF itself, with a Content-Type of application/pdf.
{
"success": true,
"pdf": "JVBERi0xLjQKJeLjz9MKMyAwIG9...",
"creditsRemaining": 9,
"metadata": { "size": 42813, "processingTime": 2841 }
}A 402 means you are out of credits, a 401 means the key is wrong, and a 422 means the markup could not be rendered. Everything else is worth retrying once.
The usual Node.js libraries
Worth knowing before you add a dependency, because for a lot of documents one of these is the right answer and costs nothing.
| Library | What it is | The trade-off |
|---|---|---|
| Puppeteer | Drives a real headless Chrome and calls page.pdf(). | Full CSS and JavaScript support, because it is Chrome. You also ship a few hundred megabytes of browser, keep it alive, and handle the memory it leaks under load. |
| Playwright | Same idea as Puppeteer, with more browsers and a nicer API. | Same operational weight. PDF output is Chromium only. |
| wkhtmltopdf | A command line tool built on an old WebKit. | Small and fast, but no longer actively maintained, and its rendering engine predates flexbox and grid, so modern CSS silently falls apart. |
| PDFKit or jsPDF | Draw the document with a drawing API: text at x, y, lines, boxes. | No HTML and no CSS. Fine for a fixed layout, painful the moment the design changes. |
Node.js: common questions
How do I convert HTML to PDF in Node.js without Puppeteer?
Post the HTML to a rendering API and write the bytes it returns. The sample above does it in about fifteen lines with the built-in fetch, no native dependency and no browser in your image.
Does it work in a serverless function?
Yes, and that is the main reason to use it. Headless Chrome barely fits in a Lambda or a Vercel function, while an HTTPS request fits anywhere. Allow around five seconds for the call.
Can I use custom fonts and CSS?
Yes. Rendering runs in headless Chrome, so Google Fonts, @font-face with absolute URLs, flexbox, grid and charts drawn by JavaScript all work.
Do I need node-fetch?
No. fetch has been global since Node 18.
Is my HTML stored anywhere?
No. It is rendered in memory, the PDF comes back in the same response, and the worker releases it. Nothing is written to disk.