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

  1. PDF libraries (PDFKit, jsPDF) - Full control, steep learning curve
  2. Headless browser (Puppeteer) - Use HTML/CSS you already know, heavy dependency
  3. 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?

ScenarioBest ApproachWhy
Simple invoices, low volumePDFKitNo dependencies, lightweight
Complex design, self-hostedPuppeteerFull CSS support, your own infra
Production SaaS, any volumePDF APINo ops burden, scales automatically
Serverless / edge functionsPDF APICan't run Puppeteer in Lambda easily

Pro Tips for Better Invoices

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 →