pdf-lib creates and modifies PDFs in pure JavaScript with no native dependencies — PDFDocument.create() creates a new document. pdfDoc.addPage([width, height]) adds a page. page.drawText("text", { x, y, size, font, color }) renders text. pdfDoc.embedFont(StandardFonts.Helvetica) or pdfDoc.embedFont(customFontBytes) loads fonts. page.drawRectangle({ x, y, width, height, color, borderColor }) draws shapes. pdfDoc.embedPng(imageBytes) and pdfDoc.embedJpg(imageBytes) embed images. PDFDocument.load(existingPdfBytes) opens existing PDFs for modification. pdfDoc.copyPages(otherDoc, [0, 1]) merges pages from another document. pdfDoc.encrypt({ userPassword, ownerPassword }) adds password protection. pdfDoc.getForm() accesses interactive AcroForm fields. pdfDoc.save() returns Uint8Array. Claude Code generates pdf-lib invoice templates, report builders, form fillers, PDF mergers, and Next.js Route Handler streaming patterns.
CLAUDE.md for pdf-lib
## pdf-lib Stack
- Version: pdf-lib >= 1.17, @pdf-lib/fontkit >= 1.1
- Create: const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage([595, 842]) — A4
- Font: pdfDoc.registerFontkit(fontkit); const font = await pdfDoc.embedFont(fontBytes)
- Standard: const font = await pdfDoc.embedFont(StandardFonts.Helvetica) — no fontkit needed
- Draw: page.drawText("text", { x, y, size: 12, font, color: rgb(0, 0, 0) })
- Shape: page.drawRectangle({ x, y, width, height, color: rgb(0.9, 0.9, 0.9) })
- Image: const img = await pdfDoc.embedPng(pngBytes); page.drawImage(img, { x, y, width, height })
- Output: const pdfBytes = await pdfDoc.save() → Buffer.from(pdfBytes)
- Modify: const pdfDoc = await PDFDocument.load(existingBytes)
Invoice Generator
// lib/pdf/invoice-generator.ts — generate invoice PDF
import { PDFDocument, StandardFonts, rgb, PageSizes } from "pdf-lib"
type LineItem = {
description: string
quantity: number
unitPrice: number
}
type InvoiceData = {
invoiceNumber: string
issueDate: Date
dueDate: Date
from: { company: string; address: string; city: string; email: string }
to: { company: string; address: string; city: string; email: string }
items: LineItem[]
notes?: string
taxRate?: number // e.g. 0.08 for 8%
}
export async function generateInvoicePDF(data: InvoiceData): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage(PageSizes.A4)
const { width, height } = page.getSize()
// Embed fonts
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
const fontRegular = await pdfDoc.embedFont(StandardFonts.Helvetica)
// Colors
const black = rgb(0, 0, 0)
const gray = rgb(0.5, 0.5, 0.5)
const lightGray = rgb(0.95, 0.95, 0.95)
const primary = rgb(0.23, 0.51, 0.96)
const margin = 50
const contentWidth = width - margin * 2
// ── Header ──────────────────────────────────────────────────
// Company name
page.drawText(data.from.company, {
x: margin,
y: height - margin,
size: 22,
font: fontBold,
color: primary,
})
// Invoice label
page.drawText("INVOICE", {
x: width - margin - fontBold.widthOfTextAtSize("INVOICE", 28),
y: height - margin,
size: 28,
font: fontBold,
color: lightGray,
})
// Invoice number and dates
page.drawText(`Invoice #${data.invoiceNumber}`, {
x: width - margin - 200,
y: height - margin - 40,
size: 11,
font: fontBold,
color: black,
})
page.drawText(
`Issue Date: ${formatDate(data.issueDate)} Due: ${formatDate(data.dueDate)}`,
{ x: width - margin - 200, y: height - margin - 58, size: 9, font: fontRegular, color: gray },
)
// Divider line
page.drawLine({
start: { x: margin, y: height - margin - 70 },
end: { x: width - margin, y: height - margin - 70 },
thickness: 1.5,
color: primary,
})
// ── From / To addresses ───────────────────────────────────────
let y = height - margin - 100
const col1 = margin
const col2 = margin + contentWidth / 2
page.drawText("FROM", { x: col1, y, size: 8, font: fontBold, color: gray })
page.drawText("TO", { x: col2, y, size: 8, font: fontBold, color: gray })
y -= 14
const addressLines = (info: typeof data.from) => [info.company, info.address, info.city, info.email]
for (const line of addressLines(data.from)) {
page.drawText(line, { x: col1, y, size: 10, font: fontRegular, color: black })
y -= 14
}
y = height - margin - 114
for (const line of addressLines(data.to)) {
page.drawText(line, { x: col2, y, size: 10, font: fontRegular, color: black })
y -= 14
}
// ── Line Items Table ──────────────────────────────────────────
y = height - margin - 210
// Table header background
page.drawRectangle({ x: margin, y: y - 4, width: contentWidth, height: 22, color: primary })
const cols = { desc: margin + 4, qty: margin + contentWidth - 200, price: margin + contentWidth - 130, total: margin + contentWidth - 60 }
const headerStyle = { y: y + 4, size: 9, font: fontBold, color: rgb(1, 1, 1) }
page.drawText("DESCRIPTION", { x: cols.desc, ...headerStyle })
page.drawText("QTY", { x: cols.qty, ...headerStyle })
page.drawText("UNIT PRICE", { x: cols.price, ...headerStyle })
page.drawText("TOTAL", { x: cols.total, ...headerStyle })
y -= 20
// Table rows
let subtotal = 0
data.items.forEach((item, i) => {
const rowTotal = item.quantity * item.unitPrice
subtotal += rowTotal
if (i % 2 === 0) {
page.drawRectangle({ x: margin, y: y - 4, width: contentWidth, height: 20, color: lightGray })
}
const rowStyle = { y: y + 3, size: 9, font: fontRegular, color: black }
page.drawText(item.description, { x: cols.desc, ...rowStyle })
page.drawText(item.quantity.toString(), { x: cols.qty, ...rowStyle })
page.drawText(formatCurrency(item.unitPrice), { x: cols.price, ...rowStyle })
page.drawText(formatCurrency(rowTotal), { x: cols.total, ...rowStyle })
y -= 20
})
// ── Totals ────────────────────────────────────────────────────
y -= 10
const tax = subtotal * (data.taxRate ?? 0)
const total = subtotal + tax
const totalsX = width - margin - 150
const drawTotal = (label: string, amount: number, bold = false) => {
page.drawText(label, { x: totalsX, y, size: 10, font: bold ? fontBold : fontRegular, color: bold ? primary : gray })
page.drawText(formatCurrency(amount), { x: totalsX + 100, y, size: 10, font: bold ? fontBold : fontRegular, color: bold ? primary : black })
y -= 18
}
drawTotal("Subtotal", subtotal)
if (data.taxRate) drawTotal(`Tax (${(data.taxRate * 100).toFixed(0)}%)`, tax)
// Total line
page.drawLine({ start: { x: totalsX, y: y + 12 }, end: { x: width - margin, y: y + 12 }, thickness: 1, color: primary })
drawTotal("TOTAL DUE", total, true)
// ── Notes ─────────────────────────────────────────────────────
if (data.notes) {
page.drawText("Notes:", { x: margin, y: 80, size: 9, font: fontBold, color: gray })
page.drawText(data.notes, { x: margin, y: 66, size: 9, font: fontRegular, color: gray })
}
// Footer
page.drawText("Thank you for your business", {
x: margin,
y: 30,
size: 9,
font: fontRegular,
color: gray,
})
return pdfDoc.save()
}
function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
}
function formatCurrency(amount: number): string {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount)
}
API Route
// app/api/pdf/invoice/route.ts — stream PDF response
import { NextRequest, NextResponse } from "next/server"
import { generateInvoicePDF } from "@/lib/pdf/invoice-generator"
import { auth } from "@clerk/nextjs/server"
import { db } from "@/lib/db"
export async function GET(request: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { searchParams } = new URL(request.url)
const orderId = searchParams.get("orderId")
if (!orderId) return NextResponse.json({ error: "orderId required" }, { status: 400 })
const order = await db.order.findFirst({
where: { id: orderId, userId },
include: { items: true, user: true },
})
if (!order) return NextResponse.json({ error: "Not found" }, { status: 404 })
const pdfBytes = await generateInvoicePDF({
invoiceNumber: order.invoiceNumber ?? orderId.slice(-8).toUpperCase(),
issueDate: order.createdAt,
dueDate: new Date(order.createdAt.getTime() + 30 * 24 * 60 * 60 * 1000),
from: { company: "Acme Corp", address: "123 Main St", city: "San Francisco, CA 94102", email: "[email protected]" },
to: { company: order.user.name, address: "", city: "", email: order.user.email },
items: order.items.map(item => ({
description: item.name,
quantity: item.quantity,
unitPrice: item.priceCents / 100,
})),
taxRate: 0.08,
})
return new NextResponse(pdfBytes, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${orderId}.pdf"`,
"Content-Length": pdfBytes.byteLength.toString(),
},
})
}
For the @react-pdf/renderer alternative when JSX-based PDF templates with React components (View, Text, Image, StyleSheet) familiar to React developers are preferred over pdf-lib’s canvas-like imperative drawing API — react-pdf is better for complex layouts, while pdf-lib is better for modifying existing PDFs or binary-level control, see the @react-pdf/renderer guide. For the puppeteer/playwright PDF alternative when generating PDFs from HTML pages with full CSS rendering (including web fonts, flexbox, grid) is needed — headless browser PDF generation supports the full web rendering stack at the cost of a larger server-side dependency, see the headless browser guide. The Claude Skills 360 bundle includes pdf-lib skill sets covering invoices, form filling, and PDF merging. Start with the free tier to try PDF generation code.