HTML to PDF in Node.js with Puppeteer: Complete Tutorial
Step-by-step guide to generating high-quality PDFs from HTML in Node.js using Puppeteer, with real-world examples for invoices and reports.
AltoUnlockPDF Team
PDF Tools Expert
Node.js is one of the most popular environments for server-side PDF generation. Whether you’re building an invoicing SaaS, a reporting dashboard, or an automated document workflow, generating PDFs from HTML templates is a core skill.
In this tutorial, we’ll build a complete HTML-to-PDF service in Node.js using Puppeteer — the most reliable option for high-quality output.
Why Puppeteer for PDF Generation?
Puppeteer spins up a headless Chromium browser and renders your HTML exactly as Google Chrome would. That means:
- Perfect CSS support — Flexbox, Grid, custom fonts, all work
- JavaScript execution — dynamic content is fully rendered
@pageCSS rules — proper margins, headers, footers- Background images and colors — correctly included
The tradeoff is size (~300MB) and memory usage. For most server-side applications, that’s acceptable.
Step 1: Setup
mkdir pdf-service && cd pdf-service
npm init -y
npm install puppeteer express
Step 2: Basic PDF Generation
// pdf-generator.js
import puppeteer from 'puppeteer';
export async function htmlToPdf(htmlContent, options = {}) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'], // needed in Docker
});
const page = await browser.newPage();
await page.setContent(htmlContent, {
waitUntil: 'networkidle0', // wait for fonts/images to load
});
const pdf = await page.pdf({
format: options.format || 'A4',
printBackground: true,
margin: {
top: options.margin || '15mm',
bottom: options.margin || '15mm',
left: '12mm',
right: '12mm',
},
displayHeaderFooter: options.footer ? true : false,
footerTemplate: options.footer || '',
});
await browser.close();
return pdf; // Buffer
}
Step 3: Build an Express API Endpoint
// server.js
import express from 'express';
import { htmlToPdf } from './pdf-generator.js';
const app = express();
app.use(express.json({ limit: '5mb' }));
app.post('/generate-pdf', async (req, res) => {
const { html, filename = 'document.pdf', options = {} } = req.body;
if (!html) {
return res.status(400).json({ error: 'HTML content is required' });
}
try {
const pdfBuffer = await htmlToPdf(html, options);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(pdfBuffer);
} catch (err) {
console.error('PDF generation error:', err);
res.status(500).json({ error: 'PDF generation failed' });
}
});
app.listen(3000, () => console.log('PDF service running on port 3000'));
Step 4: Real-World Invoice Template
function buildInvoiceHtml(invoice) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; color: #1a1a1a; padding: 40px; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.company-name { font-size: 28px; font-weight: 700; color: #dd7d24; }
.invoice-title { font-size: 32px; font-weight: 700; color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 30px; }
th { background: #f5f0e8; padding: 12px; text-align: left; font-weight: 600; }
td { padding: 12px; border-bottom: 1px solid #eee; }
.total { font-size: 20px; font-weight: 700; text-align: right; margin-top: 20px; }
@page { margin: 0; }
</style>
</head>
<body>
<div class="header">
<div class="company-name">${invoice.companyName}</div>
<div>
<div class="invoice-title">INVOICE</div>
<div>#${invoice.invoiceNumber}</div>
<div>${invoice.date}</div>
</div>
</div>
<table>
<tr><th>Description</th><th>Qty</th><th>Price</th><th>Total</th></tr>
${invoice.items.map(item => `
<tr>
<td>${item.description}</td>
<td>${item.qty}</td>
<td>$${item.price}</td>
<td>$${(item.qty * item.price).toFixed(2)}</td>
</tr>
`).join('')}
</table>
<div class="total">Total: $${invoice.total}</div>
</body>
</html>
`;
}
Step 5: Performance — Reuse the Browser
Creating a new Puppeteer browser for every request is expensive. Use a browser pool:
import puppeteer from 'puppeteer';
import genericPool from 'generic-pool';
const factory = {
create: () => puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }),
destroy: browser => browser.close(),
};
const browserPool = genericPool.createPool(factory, {
min: 1,
max: 5, // max 5 concurrent browsers
});
export async function htmlToPdfPooled(html) {
const browser = await browserPool.acquire();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await page.close();
return pdf;
} finally {
browserPool.release(browser);
}
}
Docker Deployment
FROM node:20-slim
RUN apt-get update && apt-get install -y \
chromium \
--no-install-recommends
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Conclusion
Puppeteer is the gold standard for HTML-to-PDF in Node.js. With a browser pool and a clean Express API, you can handle hundreds of PDF generations per minute on modest hardware. The official Puppeteer documentation is excellent and worth bookmarking.
For lighter workloads or client-only use cases, consider html2pdf.js instead.
Related Articles
Best WordPress PDF Plugins for Web Agencies (2026 Guide)
A complete guide for WordPress agencies on the best PDF export plugins, when to use a hosted API instead, and how to pick the right solution for every client project.
Read Article
How to Save an HTML Email as PDF (Gmail, Outlook & Apple Mail)
Step-by-step instructions to save HTML emails as PDF in every major email client — and how to handle emails that print poorly.
Read Article
Generating PDF Invoices from HTML Templates (Free & Paid Methods)
Create professional PDF invoices from HTML templates using free tools, open-source libraries, and SaaS APIs — with real code examples.
Read Article