Back to Blog
PDFNode.jsReportsDocuments

PDF Generation in Node.js Applications

Generate PDFs programmatically. From invoices to reports to certificates using various libraries and techniques.

B
Bootspring Team
Engineering
October 5, 2023
6 min read

PDFs are essential for invoices, reports, and documents that need consistent formatting. Here's how to generate them programmatically in Node.js.

Library Options#

Puppeteer/Playwright: - HTML/CSS to PDF - Best for complex layouts - Requires headless browser PDFKit: - Low-level PDF creation - Full control over output - No external dependencies pdf-lib: - Modify existing PDFs - Create from scratch - Works in browser too React-PDF: - React components for PDF - Declarative approach - Great developer experience

HTML to PDF with Puppeteer#

1import puppeteer from 'puppeteer'; 2 3async function generatePdfFromHtml(html: string): Promise<Buffer> { 4 const browser = await puppeteer.launch({ 5 headless: true, 6 args: ['--no-sandbox', '--disable-setuid-sandbox'], 7 }); 8 9 try { 10 const page = await browser.newPage(); 11 12 await page.setContent(html, { 13 waitUntil: 'networkidle0', 14 }); 15 16 const pdf = await page.pdf({ 17 format: 'A4', 18 printBackground: true, 19 margin: { 20 top: '20mm', 21 right: '20mm', 22 bottom: '20mm', 23 left: '20mm', 24 }, 25 }); 26 27 return pdf; 28 } finally { 29 await browser.close(); 30 } 31} 32 33// Invoice template 34function generateInvoiceHtml(invoice: Invoice): string { 35 return ` 36 <!DOCTYPE html> 37 <html> 38 <head> 39 <style> 40 body { 41 font-family: Arial, sans-serif; 42 font-size: 14px; 43 color: #333; 44 } 45 .header { 46 display: flex; 47 justify-content: space-between; 48 margin-bottom: 40px; 49 } 50 .invoice-number { 51 font-size: 24px; 52 font-weight: bold; 53 } 54 table { 55 width: 100%; 56 border-collapse: collapse; 57 margin: 20px 0; 58 } 59 th, td { 60 padding: 12px; 61 text-align: left; 62 border-bottom: 1px solid #ddd; 63 } 64 .total { 65 font-size: 18px; 66 font-weight: bold; 67 text-align: right; 68 margin-top: 20px; 69 } 70 </style> 71 </head> 72 <body> 73 <div class="header"> 74 <div> 75 <h1>Invoice</h1> 76 <p class="invoice-number">#${invoice.number}</p> 77 </div> 78 <div> 79 <p>Date: ${formatDate(invoice.date)}</p> 80 <p>Due: ${formatDate(invoice.dueDate)}</p> 81 </div> 82 </div> 83 84 <div class="addresses"> 85 <div> 86 <strong>From:</strong><br> 87 ${invoice.company.name}<br> 88 ${invoice.company.address} 89 </div> 90 <div> 91 <strong>To:</strong><br> 92 ${invoice.customer.name}<br> 93 ${invoice.customer.address} 94 </div> 95 </div> 96 97 <table> 98 <thead> 99 <tr> 100 <th>Description</th> 101 <th>Quantity</th> 102 <th>Unit Price</th> 103 <th>Amount</th> 104 </tr> 105 </thead> 106 <tbody> 107 ${invoice.items.map(item => ` 108 <tr> 109 <td>${item.description}</td> 110 <td>${item.quantity}</td> 111 <td>${formatCurrency(item.unitPrice)}</td> 112 <td>${formatCurrency(item.amount)}</td> 113 </tr> 114 `).join('')} 115 </tbody> 116 </table> 117 118 <div class="total"> 119 Total: ${formatCurrency(invoice.total)} 120 </div> 121 </body> 122 </html> 123 `; 124} 125 126// Usage 127app.get('/invoices/:id/pdf', async (req, res) => { 128 const invoice = await getInvoice(req.params.id); 129 const html = generateInvoiceHtml(invoice); 130 const pdf = await generatePdfFromHtml(html); 131 132 res.setHeader('Content-Type', 'application/pdf'); 133 res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.number}.pdf"`); 134 res.send(pdf); 135});

React-PDF#

1import { Document, Page, Text, View, StyleSheet, Font, renderToBuffer } from '@react-pdf/renderer'; 2 3// Register fonts 4Font.register({ 5 family: 'Inter', 6 fonts: [ 7 { src: '/fonts/Inter-Regular.ttf' }, 8 { src: '/fonts/Inter-Bold.ttf', fontWeight: 'bold' }, 9 ], 10}); 11 12const styles = StyleSheet.create({ 13 page: { 14 padding: 40, 15 fontFamily: 'Inter', 16 fontSize: 12, 17 }, 18 header: { 19 flexDirection: 'row', 20 justifyContent: 'space-between', 21 marginBottom: 40, 22 }, 23 title: { 24 fontSize: 24, 25 fontWeight: 'bold', 26 }, 27 table: { 28 width: '100%', 29 marginTop: 20, 30 }, 31 tableRow: { 32 flexDirection: 'row', 33 borderBottomWidth: 1, 34 borderBottomColor: '#eee', 35 paddingVertical: 8, 36 }, 37 tableHeader: { 38 backgroundColor: '#f5f5f5', 39 fontWeight: 'bold', 40 }, 41 col1: { width: '40%' }, 42 col2: { width: '20%' }, 43 col3: { width: '20%' }, 44 col4: { width: '20%', textAlign: 'right' }, 45 total: { 46 marginTop: 20, 47 textAlign: 'right', 48 fontSize: 16, 49 fontWeight: 'bold', 50 }, 51}); 52 53interface InvoiceDocProps { 54 invoice: Invoice; 55} 56 57function InvoiceDocument({ invoice }: InvoiceDocProps) { 58 return ( 59 <Document> 60 <Page size="A4" style={styles.page}> 61 <View style={styles.header}> 62 <View> 63 <Text style={styles.title}>Invoice</Text> 64 <Text>#{invoice.number}</Text> 65 </View> 66 <View> 67 <Text>Date: {formatDate(invoice.date)}</Text> 68 <Text>Due: {formatDate(invoice.dueDate)}</Text> 69 </View> 70 </View> 71 72 <View style={styles.table}> 73 <View style={[styles.tableRow, styles.tableHeader]}> 74 <Text style={styles.col1}>Description</Text> 75 <Text style={styles.col2}>Qty</Text> 76 <Text style={styles.col3}>Price</Text> 77 <Text style={styles.col4}>Amount</Text> 78 </View> 79 80 {invoice.items.map((item, index) => ( 81 <View key={index} style={styles.tableRow}> 82 <Text style={styles.col1}>{item.description}</Text> 83 <Text style={styles.col2}>{item.quantity}</Text> 84 <Text style={styles.col3}>{formatCurrency(item.unitPrice)}</Text> 85 <Text style={styles.col4}>{formatCurrency(item.amount)}</Text> 86 </View> 87 ))} 88 </View> 89 90 <Text style={styles.total}> 91 Total: {formatCurrency(invoice.total)} 92 </Text> 93 </Page> 94 </Document> 95 ); 96} 97 98// Generate PDF buffer 99async function generateInvoicePdf(invoice: Invoice): Promise<Buffer> { 100 return renderToBuffer(<InvoiceDocument invoice={invoice} />); 101}

PDFKit for Fine Control#

1import PDFDocument from 'pdfkit'; 2import { PassThrough } from 'stream'; 3 4async function generateReport(data: ReportData): Promise<Buffer> { 5 return new Promise((resolve, reject) => { 6 const doc = new PDFDocument({ 7 size: 'A4', 8 margin: 50, 9 }); 10 11 const buffers: Buffer[] = []; 12 const passThrough = new PassThrough(); 13 14 passThrough.on('data', (chunk) => buffers.push(chunk)); 15 passThrough.on('end', () => resolve(Buffer.concat(buffers))); 16 passThrough.on('error', reject); 17 18 doc.pipe(passThrough); 19 20 // Header 21 doc 22 .fontSize(24) 23 .font('Helvetica-Bold') 24 .text('Monthly Report', { align: 'center' }); 25 26 doc.moveDown(); 27 28 // Summary 29 doc 30 .fontSize(14) 31 .font('Helvetica') 32 .text(`Generated: ${formatDate(new Date())}`); 33 34 doc.moveDown(2); 35 36 // Table 37 const tableTop = doc.y; 38 const columns = [ 39 { header: 'Item', width: 200 }, 40 { header: 'Quantity', width: 100 }, 41 { header: 'Amount', width: 100 }, 42 ]; 43 44 // Table header 45 let x = 50; 46 doc.font('Helvetica-Bold').fontSize(12); 47 48 columns.forEach((col) => { 49 doc.text(col.header, x, tableTop); 50 x += col.width; 51 }); 52 53 // Table rows 54 doc.font('Helvetica').fontSize(11); 55 let y = tableTop + 25; 56 57 data.items.forEach((item) => { 58 x = 50; 59 doc.text(item.name, x, y); 60 x += columns[0].width; 61 doc.text(item.quantity.toString(), x, y); 62 x += columns[1].width; 63 doc.text(formatCurrency(item.amount), x, y); 64 y += 20; 65 }); 66 67 // Chart (simple bar chart) 68 doc.moveDown(4); 69 const chartTop = doc.y; 70 const chartHeight = 150; 71 const maxValue = Math.max(...data.chartData.map((d) => d.value)); 72 73 data.chartData.forEach((point, i) => { 74 const barHeight = (point.value / maxValue) * chartHeight; 75 const barX = 50 + i * 60; 76 const barY = chartTop + chartHeight - barHeight; 77 78 doc 79 .rect(barX, barY, 40, barHeight) 80 .fill('#3b82f6'); 81 82 doc 83 .fillColor('#000') 84 .fontSize(10) 85 .text(point.label, barX, chartTop + chartHeight + 5, { width: 40, align: 'center' }); 86 }); 87 88 doc.end(); 89 }); 90}

PDF with Tables (pdfmake)#

1import PdfPrinter from 'pdfmake'; 2 3const fonts = { 4 Roboto: { 5 normal: 'fonts/Roboto-Regular.ttf', 6 bold: 'fonts/Roboto-Bold.ttf', 7 italics: 'fonts/Roboto-Italic.ttf', 8 }, 9}; 10 11const printer = new PdfPrinter(fonts); 12 13async function generateTablePdf(data: TableData): Promise<Buffer> { 14 const docDefinition = { 15 content: [ 16 { text: 'Sales Report', style: 'header' }, 17 { text: `Period: ${data.period}`, style: 'subheader' }, 18 { 19 table: { 20 headerRows: 1, 21 widths: ['*', 'auto', 'auto', 'auto'], 22 body: [ 23 ['Product', 'Units', 'Revenue', 'Growth'], 24 ...data.rows.map((row) => [ 25 row.product, 26 row.units.toString(), 27 formatCurrency(row.revenue), 28 `${row.growth}%`, 29 ]), 30 ], 31 }, 32 }, 33 ], 34 styles: { 35 header: { 36 fontSize: 22, 37 bold: true, 38 margin: [0, 0, 0, 10], 39 }, 40 subheader: { 41 fontSize: 14, 42 margin: [0, 0, 0, 20], 43 }, 44 }, 45 }; 46 47 return new Promise((resolve, reject) => { 48 const pdfDoc = printer.createPdfKitDocument(docDefinition); 49 const chunks: Buffer[] = []; 50 51 pdfDoc.on('data', (chunk) => chunks.push(chunk)); 52 pdfDoc.on('end', () => resolve(Buffer.concat(chunks))); 53 pdfDoc.on('error', reject); 54 55 pdfDoc.end(); 56 }); 57}

Best Practices#

Performance: ✓ Cache static assets (fonts, images) ✓ Use worker threads for generation ✓ Queue PDF jobs for heavy workloads ✓ Stream large PDFs Quality: ✓ Embed fonts for consistency ✓ Use vector graphics when possible ✓ Test on different PDF readers ✓ Optimize file size Security: ✓ Sanitize user input in content ✓ Set appropriate permissions ✓ Don't expose generation endpoints

Conclusion#

Choose the right tool for your needs—Puppeteer for complex HTML layouts, React-PDF for declarative design, PDFKit for fine control. Queue heavy generation tasks and cache where possible.

Always test generated PDFs across different viewers for consistency.

Share this article

Help spread the word about Bootspring