HTML to PDF in Node.js
Most Node.js apps make PDFs with Puppeteer. It downloads its own Chrome and needs the right system libraries in your container, and each PDF keeps a browser tab open in your process. Here the HTML goes to the PDFDispatch API instead, and the PDF comes back, using only what is built into Node.js.
What you need
- Node.js 18 or newer (for
fetch,FormDataandBlob). - A PDFDispatch key from pdfdispatch.com/app/keys, in the environment variable
PDD_KEY.
export PDD_KEY=pdd_your_key_here
One function
// pdf.mjs
export async function htmlToPdf(html, options = {}) {
const form = new FormData();
form.append('files', new Blob([html], { type: 'text/html' }), 'index.html');
form.append('printBackground', 'true');
for (const [name, value] of Object.entries(options)) {
form.append(name, String(value));
}
const res = await fetch('https://api.pdfdispatch.com/v1/convert/html', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.PDD_KEY}` },
body: form,
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) {
throw new Error(`PDF failed: ${res.status} ${await res.text()}`);
}
return Buffer.from(await res.arrayBuffer());
}
The page must be named index.html. printBackground keeps background colours, which browsers leave out when
they print. Any other rendering option goes in options, for
example { paperWidth: "210mm", paperHeight: "297mm" } for A4.
Use it:
import { writeFile } from 'node:fs/promises';
import { htmlToPdf } from './pdf.mjs';
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 15mm; }
body { font-family: sans-serif; }
</style>
</head>
<body><h1>Monthly report</h1></body>
</html>`;
await writeFile(
'report.pdf',
await htmlToPdf(html, { preferCssPageSize: true }),
);
preferCssPageSize makes the API use the size from your @page rule.
In an Express route
import express from 'express';
import { htmlToPdf } from './pdf.mjs';
const app = express();
app.get('/orders/:id/receipt.pdf', async (req, res, next) => {
try {
const order = await findOrder(req.params.id);
const pdf = await htmlToPdf(renderReceipt(order));
res.type('application/pdf');
res.set(
'Content-Disposition',
`inline; filename="receipt-${order.id}.pdf"`,
);
res.send(pdf);
} catch (err) {
next(err);
}
});
renderReceipt is whatever makes your HTML: a template string, Handlebars, EJS, or renderToStaticMarkup from
React. The API only sees the finished HTML.
Charts and other JavaScript
Scripts on the page run before it prints. If a chart draws after the page has loaded, tell the API to wait for it. Set a flag in the page when the chart is done:
await drawChart();
window.status = 'ready';
Then send waitWindowStatus: "ready" in options. A page that never sets the flag ends in a timeout, so make sure it
also gets there when its data fails to load. Waiting has the
other ways to wait.
Images and stylesheets
Anything with a public URL loads as it would in a browser. Files that are not public go in the same request. Append
each one under files and refer to it by its bare name:
import { openAsBlob } from 'node:fs';
form.append('files', await openAsBlob('assets/logo.png'), 'logo.png');
form.append('files', await openAsBlob('assets/report.css'), 'report.css');
In the HTML, that is <img src="logo.png"> and <link rel="stylesheet" href="report.css">. (openAsBlob needs
Node.js 20; on 18, read the file with readFile and wrap it in a Blob.)
When it fails
Errors from the gateway, such as a wrong key or a used-up plan, are JSON with a numeric code. Errors in the
request, such as a missing index.html, are a 400 with a short text. Both are in Limits and
errors. Every answer has an X-Request-Id header; put it in your logs and
in any e-mail to support.
Next
- HTML to PDF: every field of this route.
- URL to PDF: print a page that is already online.
- Webhooks: get the PDF delivered to your server instead of waiting for it.