Skip to main content

Generate PDF Using PDF-lib in Node.js

· 18 min read
Michał Szymanowski
Michał Szymanowski
PDFBolt Co-Founder

Visual representation of PDF generation with PDF-lib in Node.js

If your Node.js app needs to generate or modify PDFs, pdf-lib is worth a look. It is one of several Node.js PDF libraries, each with different trade-offs. PDF‑lib is a pure JavaScript library for creating and modifying PDF documents. Unlike many alternatives, PDF-lib doesn't rely on native dependencies, making it lightweight and easy to integrate into any Node.js project. This guide covers PDF generation with PDF-lib in Node.js – from basic documents to invoices and forms.

How PDF-lib Works​

PDF-lib manipulates PDF documents directly, without external dependencies.

Here's how it works:

  • Direct PDF Manipulation: PDF-lib works directly with the PDF format itself – it does not convert HTML to PDF. This is an important distinction, as PDF-lib is designed for programmatic creation and modification of PDF documents, not for rendering web content into PDFs.

  • PDF Document Model: PDF-lib creates an in-memory model of a PDF document that mirrors the actual PDF structure. This model includes pages, content streams, objects, and metadata.

  • Document Creation Flow: When you create a PDF with PDF-lib, you typically:

    • Create a document object – PDFDocument.
    • Add pages to the document.
    • Embed fonts and other resources (images, etc.).
    • Draw content (text, shapes, images) on pages.
    • Save the document to bytes which can be written to a file.
  • Content Coordinates: PDF-lib uses a coordinate system where (0,0) is at the bottom-left corner of the page, with positive x-axis extending to the right and positive y-axis extending upward.

  • Asynchronous Operations: Many PDF-lib operations return Promises, so you can load large files without blocking.

Why Choose PDF-lib?​

Why pick PDF-lib over other options?

  • Pure JavaScript: No native dependencies or binaries required – unlike browser-based tools like Puppeteer that need Chromium.
  • TypeScript compatibility: Includes TypeScript type definitions.
  • Small footprint: Lightweight compared to alternatives like PDFKit or PDFMake.
  • Stable and widely adopted: Millions of weekly npm downloads and a large user base.
  • Wide API coverage: Supports text, images, forms, metadata, and PDF modification.

Installing PDF-lib in Node.js​

Start by adding PDF-lib to your Node.js project:

npm install pdf-lib

For TypeScript users, the types are included, so no additional packages are needed.

Basic PDF Generation with PDF-lib​

Creating a Simple PDF Document​

Here's a basic example showing how to create a PDF with text:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdf() {
// Create a new empty PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page to the document with default A4 size
const page = pdfDoc.addPage();

// Embed Helvetica font (built-in standard font)
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get page dimensions for positioning
const { width, height } = page.getSize();
const fontSize = 30;

// Place text on the page with specific coordinates
page.drawText('Hello!', {
x: 50, // Horizontal position from left edge
y: height - 100, // Vertical position from bottom edge
size: fontSize,
font: helveticaFont,
color: rgb(0, 0, 0),
});

page.drawText('Hope you have an awesome day!', {
x: 50,
y: height - 180,
size: fontSize,
font: helveticaFont,
color: rgb(0, 0, 1),
});

// Convert document to file and save
const pdfBytes = await pdfDoc.save();
await fs.writeFile('output.pdf', pdfBytes);

console.log('PDF created successfully! (output.pdf)');
}

// Handle any potential errors
createPdf().catch(err => {
console.error('Error creating PDF:', err);
});

This basic script creates a PDF with a single page and adds text to it:

Simple PDF document with some text

Adding Images to Your PDF​

You can also embed images in your PDF documents:

const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithImage() {
// Create a new empty PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page to the document
const page = pdfDoc.addPage();

// Read local image file
const imageData = await fs.readFile('image.jpg');

// Embed image into the PDF
const embeddedImage = await pdfDoc.embedJpg(imageData);

// Scale image to fit the page
const imageDimensions = embeddedImage.scale(0.4);

// Get page dimensions for positioning
const { width, height } = page.getSize();

// Draw image centered horizontally
page.drawImage(embeddedImage, {
x: (width - imageDimensions.width) / 2,
y: height - imageDimensions.height - 50,
width: imageDimensions.width,
height: imageDimensions.height,
});

// Convert document to file and save
const pdfBytes = await pdfDoc.save();
await fs.writeFile('output-with-image.pdf', pdfBytes);

console.log('PDF with image created successfully!');
}

// Handle any potential errors
createPdfWithImage().catch(err => {
console.error('Error creating PDF with image:', err);
});
PDF-lib supports JPEG and PNG formats
  • For PNG images – use pdfDoc.embedPng().
  • For JPEG – use embedJpg().

PDF document showing an embedded image

Modifying Existing PDFs with PDF-lib​

One of PDF-lib's strengths is its ability to modify existing PDF documents.

Adding a New Page​

In the example below, we load an existing PDF, add a new page to it, and place example text:

const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fs = require('fs').promises;

async function modifyPdf() {
// Load existing PDF
const existingPdfData = await fs.readFile('example.pdf');
const pdfDoc = await PDFDocument.load(existingPdfData);

// Add a new page
const page = pdfDoc.addPage();

// Embed the standard Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get page dimensions
const { width, height } = page.getSize();

// Draw text on the new page
page.drawText('This page was added using PDF-lib!', {
x: 50,
y: height - 100,
size: 30,
font: helveticaFont,
color: rgb(0, 0.53, 0.71),
});

// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.save();
await fs.writeFile('modified.pdf', modifiedPdfBytes);

console.log('PDF modified successfully!');
}

// Handle any potential errors
modifyPdf().catch(err => {
console.error('Error modifying PDF:', err);
});

Here is an example output:

Modified PDF with newly added page

Adding a Watermark to Each Page​

In this next example, we load an existing PDF and add a watermark to each page:

const { PDFDocument, StandardFonts, rgb, degrees } = require('pdf-lib');
const fs = require('fs').promises;

async function addWatermark() {
// Load existing PDF
const existingPdfData = await fs.readFile('file-sample.pdf');
const pdfDoc = await PDFDocument.load(existingPdfData);

// Embed the standard Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Get all pages in the document
const pages = pdfDoc.getPages();

// Add watermark to each page
pages.forEach(page => {
const { width, height } = page.getSize();

const watermarkText = 'CONFIDENTIAL';
const fontSize = 75;

page.drawText(watermarkText, {
x: width / 2 - 180,
y: height / 2 - 200,
size: fontSize,
font: helveticaFont,
color: rgb(0.9, 0.1, 0.1),
opacity: 0.2,
rotate: degrees(45),
});
});

// Save the modified PDF
const modifiedPdfBytes = await pdfDoc.save();
await fs.writeFile('watermarked.pdf', modifiedPdfBytes);

console.log('PDF watermarked successfully!');
}

// Handle any potential errors
addWatermark().catch(err => {
console.error('Error adding watermark:', err);
});

Example output:

PDF with diagonal watermark across the page

Merging Multiple PDFs​

Combining multiple PDF documents into one:

const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function mergePdfs() {
try {
// Create a new PDF document
const mergedPdf = await PDFDocument.create();

// Load the first PDF
const pdf1Bytes = await fs.readFile('document1.pdf');
const pdf1 = await PDFDocument.load(pdf1Bytes);

// Load the second PDF
const pdf2Bytes = await fs.readFile('document2.pdf');
const pdf2 = await PDFDocument.load(pdf2Bytes);

// Copy pages from the first document
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
for (const page of pdf1Pages) {
mergedPdf.addPage(page);
}

// Copy pages from the second document
const pdf2Pages = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
for (const page of pdf2Pages) {
mergedPdf.addPage(page);
}

// Save the merged PDF
const mergedPdfBytes = await mergedPdf.save();
await fs.writeFile('merged.pdf', mergedPdfBytes);

console.log('PDFs merged successfully!');
} catch (err) {
console.error('Error merging PDFs:', err);
if (err.code === 'ENOENT') {
console.error('One of the PDF files does not exist');
}
}
}

mergePdfs();

Creating PDF Forms with PDF-lib​

PDF-lib lets you create interactive forms in PDF documents, such as text inputs, radio buttons, checkboxes, dropdowns, and option lists.

Below is an example of how to create such a form:

Click to view the example
const { PDFDocument } = require('pdf-lib');
const fs = require('fs').promises;

async function createForm() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page with specified dimensions
const page = pdfDoc.addPage();

// Create a form for the document
const form = pdfDoc.getForm();

// Add text input field
page.drawText('Enter your most ridiculous superpower:', { x: 50, y: 800, size: 16 });
const superpowerInput = form.createTextField('superpower.input');
superpowerInput.setText('Professional Nap Taking');
superpowerInput.addToPage(page, { x: 50, y: 760, width: 440, height: 25 });

// Add radio button group
page.drawText('Choose your ultimate procrastination method:', { x: 50, y: 700, size: 16 });
page.drawText('Binge-watching cat videos', { x: 80, y: 670, size: 14 });
page.drawText('Organizing sock drawer', { x: 80, y: 640, size: 14 });
page.drawText('Scrolling memes', { x: 330, y: 670, size: 14 });
page.drawText('Inventing excuses', { x: 330, y: 640, size: 14 });

const procrastinationGroup = form.createRadioGroup('procrastination.method');
procrastinationGroup.addOptionToPage('Binge-watching cat videos', page, { x: 50, y: 670, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Organizing sock drawer', page, { x: 50, y: 640, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Scrolling memes', page, { x: 300, y: 670, width: 15, height: 15 });
procrastinationGroup.addOptionToPage('Inventing excuses', page, { x: 300, y: 640, width: 15, height: 15 });
procrastinationGroup.select('Scrolling memes');

// Add checkboxes
page.drawText('Select your dream vacation activities:', { x: 50, y: 580, size: 16 });
page.drawText('Beach lounging', { x: 80, y: 540, size: 14 });
page.drawText('Adventure sports', { x: 80, y: 510, size: 14 });
page.drawText('Cultural exploration', { x: 330, y: 540, size: 14 });
page.drawText('Foodie adventures', { x: 330, y: 510, size: 14 });

const firstActivityBox = form.createCheckBox('vacation.activity1');
const secondActivityBox = form.createCheckBox('vacation.activity2');
const thirdActivityBox = form.createCheckBox('vacation.activity3');
const fourthActivityBox = form.createCheckBox('vacation.activity4');

firstActivityBox.addToPage(page, { x: 50, y: 540, width: 15, height: 15 });
secondActivityBox.addToPage(page, { x: 50, y: 510, width: 15, height: 15 });
thirdActivityBox.addToPage(page, { x: 300, y: 540, width: 15, height: 15 });
fourthActivityBox.addToPage(page, { x: 300, y: 510, width: 15, height: 15 });

// Pre-check some activities
firstActivityBox.check();
fourthActivityBox.check();

// Add dropdown for snack selection
page.drawText('Choose your snack:', { x: 50, y: 440, size: 16 });
const snackDropdown = form.createDropdown('snack.selection');
snackDropdown.addOptions(['Pizza', 'Chocolate', 'Nachos', 'Ice Cream']);
snackDropdown.select('Nachos');
snackDropdown.addToPage(page, { x: 50, y: 400, width: 440, height: 25 });

// Add option list
page.drawText('Select your ultimate awkward moment:', { x: 50, y: 340, size: 16 });
const awkwardMomentList = form.createOptionList('awkward.moment');
awkwardMomentList.addOptions([
'Waving back at no one',
'Laughing alone in public',
'Talking to yourself',
'Tripping on flat ground',
'Misheard conversation'
]);
awkwardMomentList.select('Tripping on flat ground');
awkwardMomentList.addToPage(page, { x: 50, y: 220, width: 440, height: 100 });

// Save the PDF document
const pdfBytes = await pdfDoc.save();
await fs.writeFile('form.pdf', pdfBytes);
console.log('Hilarious form created successfully!');
}

// Run the function and handle any errors
createForm().catch(console.error);

Here is the preview of output:

Interactive PDF form with text inputs, radio buttons, checkboxes, and dropdown menus

Click to view the interactive PDF

Adding Metadata to Your PDF​

Metadata like title, author, and keywords makes your PDFs easier to find and organize.

Here's how to add metadata to a PDF:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdf() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page
const page = pdfDoc.addPage();

// Embed Helvetica font
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

// Place some text on the page
page.drawText('This document includes metadata information.', {
x: 50,
y: 750,
size: 24,
font: helveticaFont,
color: rgb(0, 0.5, 0),
});

// Set document metadata
pdfDoc.setTitle('The Coding Adventure');
pdfDoc.setAuthor('Curious Developer');
pdfDoc.setSubject('Exploring PDF Generation');
pdfDoc.setKeywords(['pdf-lib', 'nodejs', 'metadata']);
pdfDoc.setCreator('Code Craft Workshop');
pdfDoc.setProducer('Learning Project');

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('metadata.pdf', pdfBytes);

console.log('PDF with metadata created successfully!');
}

// Handle any potential errors
createPdf().catch(err => {
console.error('Error creating PDF:', err);
});

Drawing Shapes and Lines in PDF-lib​

You can also draw shapes with PDF-lib:

const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithShapes() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a blank page
const page = pdfDoc.addPage();

// Draw a rectangle
page.drawRectangle({
x: 50,
y: 700,
width: 100,
height: 100,
color: rgb(1, 0, 0), // Red fill
borderColor: rgb(0, 0, 0), // Black border
borderWidth: 2,
opacity: 0.5, // Semi-transparent
});

// Draw a circle
const centerX = 300;
const centerY = 700;
const radius = 50;

page.drawCircle({
x: centerX,
y: centerY,
size: radius,
color: rgb(0, 0, 1), // Blue fill
borderColor: rgb(0, 0, 0), // Black border
borderWidth: 2,
});

// Draw lines
page.drawLine({
start: { x: 50, y: 550 },
end: { x: 500, y: 550 },
thickness: 3,
color: rgb(0, 0, 0),
dashArray: [10, 5], // Dashed line
});

// Draw a solid line
page.drawLine({
start: { x: 50, y: 500 },
end: { x: 500, y: 500 },
thickness: 3,
color: rgb(0, 0.7, 0.5)
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('shapes.pdf', pdfBytes);

console.log('PDF with shapes created successfully!');
}

// Handle any potential errors
createPdfWithShapes().catch(err => {
console.error('Error creating PDF:', err);
});

Here is the output:

PDF showing various shapes including rectangle, circle, and lines with different styling

Handling Text Wrapping in PDF-lib​

PDF-lib's drawText method accepts a maxWidth parameter that wraps text within a given width. This example shows how to use it:

const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fs = require('fs').promises;

async function createPdfWithWrappedText() {
try {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a new page
const page = pdfDoc.addPage();

// Get page dimensions
const { width, height } = page.getSize();

// Embed a standard font
const font = await pdfDoc.embedFont(StandardFonts.TimesRoman);

// Sample text to be wrapped
const text = 'This is a long paragraph that will be automatically wrapped to ' +
'fit within the specified width. PDF-lib provides options for controlling ' +
'the line height and text alignment. This makes it easier to create ' +
'documents with well-formatted text that remains readable.';

// Draw text
page.drawText(text, {
x: 50,
y: height - 50,
size: 14,
font: font,
color: rgb(0, 0, 0),
maxWidth: width - 100,
lineHeight: 16
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('wrapped-text.pdf', pdfBytes);

console.log('PDF with wrapped text created successfully!');
} catch (err) {
console.error('Error creating PDF with wrapped text:', err);
}
}

createPdfWithWrappedText();

Here is the output:

PDF showing text paragraph with proper line wrapping


Advanced Layout Options

For more advanced layout use cases, explore the official PDF-lib documentation. If you need automatic pagination with rich text layout, PDFMake offers a declarative approach, or you can use HTML and CSS for layout via an API.

Real-World Example: Creating a Professional Invoice​

Here's a more complete example: building an invoice PDF. It combines text, images, a table, and styled layout.

Click to view complete code
const {PDFDocument, rgb, StandardFonts, degrees} = require('pdf-lib');
const fs = require('fs').promises;

// Invoice data
const invoiceData = {
// Company information
company: {
name: 'AWESOME COMPANY',
details: [
'Awesome Company Inc.',
'123 Business Avenue, Suite 100',
'San Francisco, CA 94107',
'[email protected]',
'+1 (555) 123-4567'
],
logo: 'logo.png', // Path to company logo
website: 'www.awesomecompany.com'
},

// Invoice details
invoice: {
number: 'INV-2025-0427',
date: 'April 27, 2025',
dueDate: 'May 27, 2025',
paymentTerms: 'Net 30'
},

// Client information
client: {
name: 'Client Company LLC',
details: [
'Client Company LLC',
'Attn: John Smith',
'456 Client Street',
'New York, NY 10001',
'[email protected]'
]
},

// Invoice items
items: [
{id: 1, description: 'Website Development Services', quantity: 1, amount: 3500.00},
{id: 2, description: 'UI/UX Design', quantity: 1, amount: 1200.00},
{id: 3, description: 'Content Creation', quantity: 10, amount: 800.00},
{id: 4, description: 'Hosting Setup (Annual)', quantity: 1, amount: 240.00}
],

// Tax rate
taxRate: 0.08, // 8%

// Payment information
paymentInfo: [
'Bank: International Bank of Example',
'Account Name: Awesome Company Inc.',
'Account Number: 1234567890',
'Payment Terms: Net 30'
],

// Notes
notes: 'Thank you for your business! Please make payment by the due date.'
};

// Define colors
const colors = {
primary: rgb(0.3, 0.4, 0.6),
lightGray: rgb(0.95, 0.95, 0.95),
gray: rgb(0.8, 0.8, 0.8),
textColor: rgb(0.1, 0.1, 0.1),
white: rgb(1, 1, 1)
};

// Font sizes
const fontSizes = {
title: 24,
heading: 14,
normal: 12,
small: 10
};

async function createInvoice() {
// Create a new PDF document
const pdfDoc = await PDFDocument.create();

// Add a page
const page = pdfDoc.addPage();

// Embed fonts
const fonts = {
bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
oblique: await pdfDoc.embedFont(StandardFonts.HelveticaOblique)
};

// Get page dimensions for positioning
const {width, height} = page.getSize();

// Define margins and spacing
const layout = {
leftMargin: 40,
rightMargin: width - 40,
topMargin: height - 140,
headerHeight: 140,
detailLineHeight: 15,
rowHeight: 20,
footerHeight: 40
};

// Draw header background
page.drawRectangle({
x: 0,
y: layout.topMargin,
width: width,
height: layout.headerHeight,
color: colors.primary
});

// Draw company name
page.drawText(invoiceData.company.name, {
x: layout.leftMargin,
y: height - 50,
size: fontSizes.title,
font: fonts.bold,
color: colors.white
});

// Load and embed the logo
try {
const logoBytes = await fs.readFile(invoiceData.company.logo);
const logoImage = await pdfDoc.embedPng(logoBytes);
const logoDims = logoImage.scale(0.04); // Scale down the logo if needed

page.drawImage(logoImage, {
x: layout.leftMargin * 3,
y: height - 135,
width: logoDims.width,
height: logoDims.height
});
} catch (error) {
console.error('Error loading logo:', error);
}

// Draw Invoice text on the right side
const invoiceTextX = width - 230;
page.drawText('INVOICE', {
x: invoiceTextX,
y: height - 50,
size: fontSizes.title,
font: fonts.bold,
color: colors.white
});

// Invoice details on the right
let invoiceDetailY = height - 80;

page.drawText(`Invoice No: ${invoiceData.invoice.number}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

invoiceDetailY -= layout.detailLineHeight;
page.drawText(`Date: ${invoiceData.invoice.date}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

invoiceDetailY -= layout.detailLineHeight;
page.drawText(`Due Date: ${invoiceData.invoice.dueDate}`, {
x: invoiceTextX,
y: invoiceDetailY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.white
});

// Set up billing sections
const billingY = height - 200;

// Bill To section
page.drawText('BILL TO:', {
x: layout.leftMargin,
y: billingY,
size: fontSizes.heading,
font: fonts.bold,
color: colors.textColor
});

let clientY = billingY - 20;
invoiceData.client.details.forEach((line) => {
page.drawText(line, {
x: layout.leftMargin,
y: clientY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
clientY -= layout.detailLineHeight;
});

// Bill From section
const billFromX = width - 230;
page.drawText('BILL FROM:', {
x: billFromX,
y: billingY,
size: fontSizes.heading,
font: fonts.bold,
color: colors.textColor
});

let companyY = billingY - 20;
invoiceData.company.details.forEach((line) => {
page.drawText(line, {
x: billFromX,
y: companyY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
companyY -= layout.detailLineHeight;
});

// Service/Product table
const tableTop = billingY - 140;
const tableWidth = width - 80;
const colWidths = [40, 280, 100, 100]; // ID, Description, Quantity, Amount

// Table header row
page.drawRectangle({
x: layout.leftMargin,
y: tableTop - layout.rowHeight,
width: tableWidth,
height: layout.rowHeight,
color: colors.lightGray
});

const headers = ['#', 'Description', 'Quantity', 'Amount'];
let xOffset = layout.leftMargin;

headers.forEach((header, i) => {
page.drawText(header, {
x: xOffset + 5,
y: tableTop - 15,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});
xOffset += colWidths[i];
});

// Table rows
let rowY = tableTop - 40;

invoiceData.items.forEach((item, rowIndex) => {
// Alternating row colors
if (rowIndex % 2 === 1) {
page.drawRectangle({
x: layout.leftMargin,
y: rowY - 5,
width: tableWidth,
height: layout.rowHeight,
color: colors.lightGray
});
}

let rowX = layout.leftMargin;

// Item ID
page.drawText(item.id.toString(), {
x: rowX + 5,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[0];

// Description
page.drawText(item.description, {
x: rowX + 5,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[1];

// Quantity (right-aligned)
page.drawText(item.quantity.toString(), {
x: rowX + colWidths[2] - 60,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});
rowX += colWidths[2];

// Amount (right-aligned with currency formatting)
page.drawText(`${item.amount.toFixed(2)}`, {
x: rowX + colWidths[3] - 60,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

rowY -= layout.rowHeight;
});

// Calculate totals
const subtotal = invoiceData.items.reduce((sum, item) => sum + item.amount, 0);
const tax = subtotal * invoiceData.taxRate;
const total = subtotal + tax;

// Summary section with totals
const summaryX = 350;
const amountX = width - 100;
rowY -= 10;

page.drawText('Subtotal:', {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

page.drawText(`${subtotal.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

rowY -= layout.rowHeight;

page.drawText(`Tax (${(invoiceData.taxRate * 100).toFixed(0)}%):`, {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

page.drawText(`${tax.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.regular,
color: colors.textColor
});

// Draw line above totals
page.drawLine({
start: {x: 330, y: rowY - 10},
end: {x: layout.rightMargin, y: rowY - 10},
thickness: 1,
color: colors.gray
});

rowY -= 30;

page.drawText('Total:', {
x: summaryX,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

page.drawText(`${total.toFixed(2)}`, {
x: amountX,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

// Draw line
page.drawLine({
start: {x: layout.leftMargin, y: rowY - 30},
end: {x: layout.rightMargin, y: rowY - 30},
thickness: 1,
color: colors.gray
});

// Payment information
rowY -= 60;
const paymentInfoTitle = 'Payment Information:';

page.drawText(paymentInfoTitle, {
x: layout.leftMargin,
y: rowY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

rowY -= layout.rowHeight;

invoiceData.paymentInfo.forEach((line, index) => {
page.drawText(line, {
x: layout.leftMargin,
y: rowY - (index * 15),
size: fontSizes.small,
font: fonts.regular,
color: colors.textColor
});
});

// Notes
const notesY = rowY - 80;
const notesTitle = 'Notes:';

page.drawText(notesTitle, {
x: layout.leftMargin,
y: notesY,
size: fontSizes.normal,
font: fonts.bold,
color: colors.textColor
});

page.drawText(invoiceData.notes, {
x: layout.leftMargin,
y: notesY - 20,
size: fontSizes.small,
font: fonts.oblique,
color: colors.textColor
});

// Footer with light background
const footerContent = `${invoiceData.company.name} | ${invoiceData.company.website}`;

page.drawRectangle({
x: 0,
y: 0,
width: width,
height: layout.footerHeight,
color: colors.lightGray
});

page.drawText(footerContent, {
x: width / 2 - 120,
y: 15,
size: fontSizes.small,
font: fonts.regular,
color: colors.textColor
});

// Save the PDF
const pdfBytes = await pdfDoc.save();
await fs.writeFile('invoice.pdf', pdfBytes);

console.log('Professional invoice created successfully!');
}

// Handle any potential errors
createInvoice().catch(err => {
console.error('Error creating invoice:', err);
});

This example creates a complete invoice with:

  1. Branded header and logo.
  2. Company information.
  3. Client billing details.
  4. Itemized table with product/service descriptions.
  5. Financial calculations for subtotal, tax, and total amount.
  6. Payment information section.
  7. Professional footer.

Here's the generated invoice:

Professional invoice with company branding, client details, and itemized billing table

Best Practices for PDF Generation with PDF-lib​

PracticeDescription
Reuse Font EmbeddingsCall embedFont() once and pass the result to every drawText(). Re-embedding the same font per page wastes memory.
Error HandlingWrap PDFDocument.load() in try-catch – corrupted or password-protected PDFs will throw. Validate file existence before fs.readFile().
Coordinate HelpersWrite small helpers for common positions (margins, centered text). PDF-lib's bottom-left origin makes manual math error-prone.
Image FormatUse JPEG for photos and PNG for graphics with transparency. Avoid large uncompressed PNGs – they inflate file size.
MetadataAlways set setTitle(), setAuthor(), and setCreationDate(). This helps PDF readers display document info and improves indexing.
TestingCompare generated PDF byte output against a known-good snapshot, or use pdf-parse to extract and assert text content.

Alternative: Template-Based PDF Generation​

PDF-lib gives you fine-grained document control, but managing all that coordinate math adds code to your application. For projects where simplicity and scalability matter more, template-based PDF generation APIs like PDFBolt let you design once in HTML/CSS and generate at scale.

Invoice Example with PDFBolt API​

Here's how the same invoice from our PDF-lib example would look using PDFBolt's template approach:

Step 1: Design Your Template

  1. Create an account at PDFBolt.
  2. Use the visual template designer to build your invoice layout with HTML/CSS.
  3. Add Handlebars placeholders like {{company.name}} and {{items}} for dynamic content.
  4. Test with sample data, publish, and get your template ID.

Step 2: Generate PDFs with Simple API Calls

Here's how the same invoice generation would look using PDFBolt's template system:

Node.js Implementation
const fs = require('fs');

async function generatePdf() {
const response = await fetch('https://api.pdfbolt.com/v1/direct', {
method: 'POST',
headers: {
'API-KEY': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
'Content-Type': 'application/json'
},
body: JSON.stringify({
templateId: 'your-template-id',
templateData: {
"company": {
"name": "Awesome Company Inc.",
"details": [
"123 Business Avenue, Suite 100",
"San Francisco, CA 94107",
"[email protected]",
"+1 (555) 123-4567"
],
"website": "www.awesomecompany.com"
},
"invoice": {
"number": "INV-2025-0427",
"date": "April 27, 2025",
"dueDate": "May 27, 2025",
"paymentTerms": "Net 30"
},
"client": {
"name": "Client Company LLC",
"details": [
"Attn: John Smith",
"456 Client Street",
"New York, NY 10001",
"[email protected]"
]
},
"items": [
{
"id": 1,
"description": "Website Development Services",
"quantity": 1,
"amount": 3500.0
},
{
"id": 2,
"description": "UI/UX Design",
"quantity": 1,
"amount": 1200.0
},
{
"id": 3,
"description": "Content Creation",
"quantity": 10,
"amount": 800.0
},
{
"id": 4,
"description": "Hosting Setup (Annual)",
"quantity": 1,
"amount": 240.0
}
],
"taxRate": 0.08,
"paymentInfo": [
"Bank: International Bank of Example",
"Account Name: Awesome Company Inc.",
"Account Number: 1234567890",
"Payment Terms: Net 30"
],
"summary": {
"subtotal": 5740.0,
"tax": 459.2,
"total": 6199.2
},
"notes": "Thank you for your business! Please make payment by the due date."
}

})
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status} - ${errorText}`);
}

const pdfBuffer = await response.arrayBuffer();
fs.writeFileSync('invoice_pdfbolt.pdf', Buffer.from(pdfBuffer));
console.log('PDF generated successfully');
}

generatePdf().catch(console.error);

This approach moves the layout work from code to a visual template designer, which can simplify your application code without sacrificing output quality.

Explore Template-Based PDF Generation

Learn more about API-driven PDF creation:

Additional PDF Generation Methods – beyond templates, PDFBolt also supports:

Explore HTML to PDF and URL to PDF conversion methods.

Conclusion​

PDF-lib is a solid, dependency-free library for generating and modifying PDFs in Node.js. Its pure JavaScript implementation means no Chromium binary, no native add-ons – just npm install and go. The trade-off is that you manage every coordinate, font embedding, and page break yourself.

That works well for invoices, forms, watermarks, and PDF merging. For other use cases, consider these alternatives:

  • HTML to PDF conversion: Puppeteer or Playwright render full HTML/CSS into PDFs.
  • Declarative layouts: PDFMake uses JSON document definitions with automatic pagination.
  • High-volume generation: A dedicated HTML to PDF API handles rendering and scaling for you.

For a side-by-side comparison of all these options, see Top Node.js PDF Generation Libraries Compared.

Remember: life is like a PDF – sometimes you need to add a little structure to make everything look good! 🎨