Claude Code for pdf-lib: PDF Generation in Pure JavaScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for pdf-lib: PDF Generation in Pure JavaScript
Backend

Claude Code for pdf-lib: PDF Generation in Pure JavaScript

Published: April 23, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free