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.