February 16, 2026 · 10 min read
How to Generate PDF Invoices with Node.js
Every SaaS eventually needs to generate invoices. Here are 3 approaches, from DIY to fully managed, with real code you can copy.
The 3 Approaches
- PDF libraries (PDFKit, jsPDF) - Full control, steep learning curve
- Headless browser (Puppeteer) - Use HTML/CSS you already know, heavy dependency
- HTML-to-PDF API (PDFMagic, DocRaptor) - One HTTP call, no dependencies
Approach 1: PDFKit (DIY Library)
PDFKit gives you pixel-level control but requires manual positioning of every element.
const PDFDocument = require('pdfkit');
const fs = require('fs');
function generateInvoice(invoice) {
const doc = new PDFDocument({ margin: 50 });
doc.pipe(fs.createWriteStream(`invoice-${invoice.number}.pdf`));
// Header
doc.fontSize(20).text('INVOICE', 50, 50);
doc.fontSize(10).text(`#${invoice.number}`, 50, 75);
doc.text(`Date: ${invoice.date}`, 50, 90);
// Customer info
doc.fontSize(12).text(invoice.customer.name, 50, 130);
doc.fontSize(10).text(invoice.customer.email, 50, 148);
// Line items
let y = 200;
doc.fontSize(10).font('Helvetica-Bold');
doc.text('Item', 50, y).text('Qty', 300, y).text('Price', 400, y).text('Total', 470, y);
y += 20;
doc.font('Helvetica');
invoice.items.forEach(item => {
const total = item.quantity * item.price;
doc.text(item.description, 50, y)
.text(item.quantity.toString(), 300, y)
.text(`$${item.price.toFixed(2)}`, 400, y)
.text(`$${total.toFixed(2)}`, 470, y);
y += 20;
});
// Total
y += 10;
doc.font('Helvetica-Bold')
.text(`Total: $${invoice.total.toFixed(2)}`, 400, y);
doc.end();
}
Pros: No external dependencies, small bundle, full control.
Cons: Manual layout is tedious. Complex invoices with tables, logos, and styling take hundreds of lines. Updating the design means rewriting coordinate math.
Approach 2: Puppeteer (HTML to PDF)
Write your invoice as HTML/CSS, render it with Puppeteer, export to PDF. This is the most popular approach.
const puppeteer = require('puppeteer');
async function generateInvoice(html) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' }
});
await browser.close();
return pdf;
}
// Your invoice HTML - use any CSS framework
const invoiceHtml = `
<html>
<style>
body { font-family: 'Helvetica', sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #eee; text-align: left; }
.total { font-size: 1.2em; font-weight: bold; }
</style>
<body>
<h1>Invoice #1001</h1>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
<tr><td>Pro Plan</td><td>1</td><td>$29.00</td></tr>
</table>
<p class="total">Total: $29.00</p>
</body>
</html>`;
generateInvoice(invoiceHtml).then(pdf => {
require('fs').writeFileSync('invoice.pdf', pdf);
});
Pros: Use HTML/CSS you know. Easy to add logos, colors, complex layouts.
Cons: Puppeteer is ~300MB+ installed. Cold starts take 2-5s. Memory-hungry. Not great for serverless or high-volume.
Approach 3: HTML-to-PDF API (Recommended)
Send your HTML, get back a PDF. No Puppeteer dependency, no Chrome binary, no memory issues. One HTTP call.
const axios = require('axios');
const fs = require('fs');
async function generateInvoice(html) {
const response = await axios.post('https://pdf.grabshot.dev/api/convert', {
html: html,
format: 'A4',
margin: { top: '20mm', bottom: '20mm' }
}, {
headers: { 'X-API-Key': 'your-api-key' },
responseType: 'arraybuffer'
});
fs.writeFileSync('invoice.pdf', response.data);
}
// Same HTML as Approach 2 - but no Puppeteer needed!
generateInvoice(invoiceHtml);
Pros: Zero dependencies. Works in serverless (Lambda, Vercel, Cloudflare Workers). Fast (~1-2s). Scales to any volume.
Cons: Costs money at scale. Requires network call (not offline).
Which Approach Should You Use?
| Scenario | Best Approach | Why |
|---|---|---|
| Simple invoices, low volume | PDFKit | No dependencies, lightweight |
| Complex design, self-hosted | Puppeteer | Full CSS support, your own infra |
| Production SaaS, any volume | PDF API | No ops burden, scales automatically |
| Serverless / edge functions | PDF API | Can't run Puppeteer in Lambda easily |
Pro Tips for Better Invoices
- Use @page CSS rules for headers/footers and page numbers in Puppeteer/API approaches
- Embed fonts - don't rely on system fonts. Use Google Fonts with @import or embed as base64
- Test print margins - what looks good on screen may clip when printed
- Add a machine-readable component - include a QR code with invoice data for accounting software integration
- Buffer.from() the response - API responses may return Uint8Array, not Buffer. Wrap with Buffer.from() to avoid issues
Try PDFMagic - HTML to PDF API
Convert any HTML to pixel-perfect PDFs with one API call. Free tier included. Built by the GrabShot team.
Get Your Free API Key →