Fastify plugin for PDF generation and manipulation. Convert HTML/Markdown to PDF, fill forms, and merge PDFs.
> Fastify v5 plugin for PDF generation and manipulation with a simple, intuitive library of methods.
Generate PDFs from HTML and Markdown, fill PDF forms, and merge PDFs. Works with any Fastify application and optionally integrates with xStorage for automatic cloud storage.
- Fastify v5.0.0+
- Node.js v20+
- 📄 HTML to PDF - Convert HTML to PDF using Puppeteer
- 📝 Markdown to PDF - Convert Markdown to PDF with styled formatting
- 📋 Form Filling - Fill PDF form fields with values
- ✏️ Form Flattening - Make form fields non-editable
- 🔗 PDF Merging - Combine multiple PDFs into one
- 💾 Optional Storage - Save PDFs to S3-compatible storage via xStorage
- 🔌 Simple API - Methods decorated on Fastify instance
- 🎯 Library Pattern - Use as a service, not as HTTP routes
``bash`
npm install @xenterprises/fastify-xpdf puppeteer marked pdf-lib fastify@5
For optional storage integration:
`bash`
npm install @xenterprises/fastify-xstorage
`javascript
import Fastify from "fastify";
import xPDF from "@xenterprises/fastify-xpdf";
const fastify = Fastify({ logger: true });
// Register xPDF
await fastify.register(xPDF, {
headless: true,
useStorage: false,
defaultFolder: "pdfs",
});
// Generate PDF from HTML
const result = await fastify.xPDF.generateFromHtml("
console.log(result);
// {
// buffer: Buffer,
// filename: "hello.pdf",
// size: 12345,
// storageKey?: "pdfs/hello.pdf", // Only if saveToStorage: true
// url?: "https://..." // Only if saveToStorage: true
// }
await fastify.listen({ port: 3000 });
`
`javascript
await fastify.register(xPDF, {
// Puppeteer options
headless: true, // Default: true
args: ["--no-sandbox"], // Chrome launch args
// xStorage integration (optional)
useStorage: false, // Default: false
defaultFolder: "pdfs", // Default storage folder
// PDF generation defaults
format: "A4", // Default page format
printBackground: true, // Include background in PDFs
margin: { // Default margins
top: "1cm",
right: "1cm",
bottom: "1cm",
left: "1cm",
},
});
`
All methods are available on the fastify.xPDF namespace.
Convert HTML to PDF.
`javascript
const result = await fastify.xPDF.generateFromHtml("Invoice
", {
filename: "invoice.pdf",
format: "A4",
landscape: false,
margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" },
printBackground: true,
saveToStorage: false,
folder: "pdfs",
});
console.log(result);
// {
// buffer: Buffer, // PDF content as Buffer
// filename: "invoice.pdf", // Output filename
// size: 245678, // File size in bytes
// storageKey?: "pdfs/...", // Only if saveToStorage: true
// url?: "https://..." // Only if saveToStorage: true
// }
`
Options:
- filename - Output filename (default: generated-{timestamp}.pdf)format
- - Page format: A4, Letter, Legal, A3, A5, etc. (default: A4)landscape
- - Landscape orientation (default: false)margin
- - Page margins (default: 1cm all sides)printBackground
- - Include background graphics (default: true)saveToStorage
- - Save to xStorage (default: false)folder
- - Storage folder (default: 'pdfs')
Convert Markdown to PDF.
`javascript
const markdown =
Date: January 1, 2024
1. Item 1: $100
2. Item 2: $200
Total: $300;
const result = await fastify.xPDF.generateFromMarkdown(markdown, {
filename: "invoice.pdf",
saveToStorage: true,
folder: "invoices",
});
console.log(result);
// {
// buffer: Buffer,
// filename: "invoice.pdf",
// size: 234567,
// storageKey: "invoices/invoice-1234567890.pdf",
// url: "https://storage.example.com/invoices/invoice-1234567890.pdf"
// }
`
Supports:
- Headings (H1-H6)
- Paragraphs and line breaks
- Lists (ordered and unordered)
- Code blocks with syntax highlighting
- Tables
- Blockquotes
- Links
- Emphasis (bold, italic)
Fill PDF form fields and optionally flatten.
`javascript
// Download PDF from storage
const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
// Fill form
const result = await fastify.xPDF.fillForm(
pdfBuffer,
{
firstName: "John",
lastName: "Doe",
email: "john@example.com",
agreeToTerms: true,
},
{
flatten: true, // Make fields non-editable
filename: "application-filled.pdf",
saveToStorage: true,
folder: "submitted-forms",
}
);
console.log(result);
// {
// buffer: Buffer,
// filename: "application-filled.pdf",
// size: 256789,
// storageKey: "submitted-forms/...",
// url: "https://..."
// }
`
Options:
- flatten - Make form fields non-editable (default: true)filename
- - Output filenamesaveToStorage
- - Save to xStorage (default: false)folder
- - Storage folder (default: 'pdfs')
Supported Field Types:
- Text fields: field.setText(value)field.check()
- Checkboxes: or field.uncheck()field.select(value)
- Radio buttons: field.select(value)
- Dropdowns:
List all form fields in a PDF.
`javascript
const pdfBuffer = await fastify.xStorage.download("forms/application.pdf");
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
console.log(fields);
// [
// { name: "firstName", type: "text", value: null },
// { name: "lastName", type: "text", value: null },
// { name: "email", type: "text", value: null },
// { name: "agreeToTerms", type: "checkbox", value: false }
// ]
`
Returns:
- name - Field nametype
- - Field type (text, checkbox, radio, dropdown, option)value
- - Current field value
Merge multiple PDFs into one.
`javascript
// Download PDFs from storage
const page1 = await fastify.xStorage.download("documents/page1.pdf");
const page2 = await fastify.xStorage.download("documents/page2.pdf");
const page3 = await fastify.xStorage.download("documents/page3.pdf");
const result = await fastify.xPDF.mergePDFs([page1, page2, page3], {
filename: "complete-document.pdf",
saveToStorage: true,
folder: "merged",
});
console.log(result);
// {
// buffer: Buffer,
// filename: "complete-document.pdf",
// size: 512345,
// pageCount: 15, // Total pages from all 3 PDFs
// storageKey: "merged/complete-document-1234567890.pdf",
// url: "https://..."
// }
`
Options:
- filename - Output filename (default: merged-{timestamp}.pdf)saveToStorage
- - Save to xStorage (default: false)folder
- - Storage folder (default: 'pdfs')
Returns:
- buffer - Merged PDF bufferfilename
- - Output filenamesize
- - File size in bytespageCount
- - Total number of pagesstorageKey
- - Storage key (if saved)url
- - Public URL (if saved)
xPDF works seamlessly with xStorage for automatic cloud storage of generated PDFs.
`javascript
import Fastify from "fastify";
import xStorage from "@xenterprises/fastify-xstorage";
import xPDF from "@xenterprises/fastify-xpdf";
const fastify = Fastify();
// Register xStorage first
await fastify.register(xStorage, {
endpoint: "https://nyc3.digitaloceanspaces.com",
region: "nyc3",
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET,
bucket: "my-bucket",
publicUrl: "https://my-bucket.nyc3.digitaloceanspaces.com",
});
// Register xPDF (will auto-detect xStorage)
await fastify.register(xPDF, {
useStorage: true,
defaultFolder: "pdfs",
});
// Now all PDFs can be saved to storage
const result = await fastify.xPDF.generateFromHtml("
console.log(result.url); // https://my-bucket.nyc3.digitaloceanspaces.com/reports/...
`
`javascript
// Get PDF from storage
const storedUrl = "https://my-bucket.nyc3.digitaloceanspaces.com/forms/template.pdf";
const key = storedUrl.replace(process.env.STORAGE_PUBLIC_URL + "/", "");
const pdfBuffer = await fastify.xStorage.download(key);
// Fill form and save result
const result = await fastify.xPDF.fillForm(pdfBuffer, { name: "John" }, {
saveToStorage: true,
folder: "submitted",
});
console.log(result.url); // Direct link to filled form
`
`javascript
import { helpers } from "@xenterprises/fastify-xpdf";
// Generate unique PDF filename
helpers.generatePdfFilename("invoice");
// "invoice-1704067200000.pdf"
// Validate PDF buffer
helpers.isValidPdfBuffer(buffer);
// true/false
// Get PDF metadata
helpers.getPdfMetadata(buffer);
// { size: 12345 }
// Sanitize filename
helpers.sanitizeFilename("My File (2024).pdf");
// "my_file_2024.pdf"
// Get page format dimensions
helpers.getPageFormat("A4");
// { width: 8.27, height: 11.7 }
`
`javascript
fastify.post("/invoices", async (request, reply) => {
const { items, total, date } = request.body;
const html =
Date: ${date}
- ${item.name}: $${item.price}
).join("")}; const result = await fastify.xPDF.generateFromHtml(html, {
filename: "invoice.pdf",
saveToStorage: true,
folder: "invoices",
});
return { success: true, url: result.url };
});
`$3
`javascript
fastify.post("/applications/:id/submit", async (request, reply) => {
const { id } = request.params;
const formData = request.body; // Get form template
const template = await fastify.xStorage.download("forms/application-template.pdf");
// Fill with submitted data
const result = await fastify.xPDF.fillForm(template, formData, {
flatten: true,
filename:
application-${id}.pdf,
saveToStorage: true,
folder: "applications",
}); // Save record in database
await fastify.db.application.create({
id,
pdfKey: result.storageKey,
pdfUrl: result.url,
submittedAt: new Date(),
});
return { success: true, pdfUrl: result.url };
});
`$3
`javascript
fastify.post("/reports/compile", async (request, reply) => {
const { reportIds } = request.body; // Get all report PDFs
const pdfBuffers = await Promise.all(
reportIds.map((id) =>
fastify.xStorage.download(
reports/${id}.pdf)
)
); // Merge into single document
const result = await fastify.xPDF.mergePDFs(pdfBuffers, {
filename: "compiled-report.pdf",
saveToStorage: true,
folder: "compiled",
});
return {
success: true,
filename: result.filename,
pages: result.pageCount,
url: result.url,
};
});
`Best Practices
1. Use xStorage for Production - Automatically manage PDF storage and access
2. Validate Input - Sanitize HTML/Markdown to prevent XSS in PDF generation
3. Handle Large PDFs - Set reasonable timeouts for Puppeteer operations
4. Organize with Folders - Use meaningful folder structures (e.g., "invoices/2024")
5. Store Metadata - Keep records of storage keys in your database
6. Error Handling - Always wrap PDF operations in try-catch blocks
7. Memory Management - PDFs are loaded in memory; handle size carefully
8. Reuse Browser - Plugin maintains single browser instance for efficiency
Configuration Profiles
$3
`javascript
await fastify.register(xPDF, {
headless: true,
useStorage: false, // Save to disk instead
format: "A4",
});
`$3
`javascript
await fastify.register(xPDF, {
headless: true,
useStorage: true,
defaultFolder: "pdfs",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
`$3
`javascript
await fastify.register(xPDF, {
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
],
useStorage: true,
});
`Troubleshooting
$3
Error:
Timeout waiting for browser to startSolution: Increase available memory and ensure Chrome dependencies are installed
`bash
macOS
brew install chromiumLinux
apt-get install -y chromium-browserOr use args to help with sandboxing
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
`$3
Error:
Form field not found: fieldNameSolution: List form fields first to verify correct names
`javascript
const fields = await fastify.xPDF.listFormFields(pdfBuffer);
console.log(fields); // Check exact field names
`$3
Error:
Timeout waiting for page to loadSolution: Simplify HTML, remove external resources, or optimize content
`javascript
// ❌ Avoid external resources
const html = '
';// ✅ Use inline or relative paths
const html = '
';
`$3
Error:
xStorage plugin not registeredSolution: Register xStorage before xPDF
`javascript
// Register xStorage FIRST
await fastify.register(xStorage, {...});// Then register xPDF
await fastify.register(xPDF, {
useStorage: true,
});
`Performance Tips
- Use
headless: true (already default)
- Enable printBackground: false for simpler documents
- Avoid large images or external resources in HTML
- Use Markdown for simple, styled documents
- Batch operations when merging many PDFsTesting
`bash
npm test
``See TESTING.md for comprehensive testing guide.
See EXAMPLES.md for complete real-world examples.
ISC