← Back to Home

Building Custom PDFs with PDFKit - A Template-Driven Approach

tags: pdfkit, pdf generation, nodejs, templates, cms integration, automation

When building applications that need to generate PDFs programmatically, developers often reach for HTML-to-PDF solutions like Puppeteer or wkhtmltopdf. While these tools work well for converting web pages, they come with significant overhead: browser dependencies, resource-intensive processes, and limited control over the final output.

Enter PDFKit - a lightweight, pure JavaScript PDF generation library that gives you pixel-perfect control without the baggage. In this article, I’ll share my experience building a production-ready PDF generation system that handles thousands of custom documents, and show you how to create a template-driven architecture that scales.\

Why PDFKit Over HTML-to-PDF Solutions?

After working with various PDF generation approaches, I’ve found PDFKit offers distinct advantages for certain use cases:

Lightweight and Fast
No browser dependencies means faster startup times and lower memory footprint. A typical PDFKit process uses ~50MB of memory compared to ~200MB+ for Puppeteer.\

Precise Layout Control
PDFKit uses PDF point coordinates (1 point = 1/72 inch), giving you exact positioning control. This is crucial for documents with specific print requirements or complex layouts.\

Predictable Output
Unlike HTML/CSS rendering which can vary between browsers, PDFKit generates consistent output every time. No more debugging CSS quirks or print margins.\

True Server-Side
Runs in pure Node.js environments without needing headless browsers. Perfect for serverless functions, Docker containers, or environments with limited resources.\

When NOT to Use PDFKit
If you need to convert existing HTML content or need complex CSS layouts, HTML-to-PDF tools are better suited. PDFKit requires you to programmatically define every element’s position.\

Understanding PDFKit’s Core Concepts

Before diving into implementation, let’s cover the fundamentals that make PDFKit powerful.\

Coordinate System

PDFKit uses a coordinate system where:\

basic-positioning.js
const PDFDocument = require('pdfkit');
const fs = require('fs');
const doc = new PDFDocument({
size: 'A4', // 595 x 842 points
margins: { top: 50, bottom: 50, left: 50, right: 50 }
});
doc.pipe(fs.createWriteStream('output.pdf'));
// Position text at exact coordinates
doc.fontSize(16)
.text('Hello World', 100, 100); // x: 100, y: 100
// Draw a rectangle at specific position
doc.rect(100, 150, 200, 50) // x, y, width, height
.stroke();
doc.end();

Drawing Primitives

PDFKit provides methods for all basic PDF elements - text, images, shapes, and paths. Each element can be precisely positioned using x,y coordinates and styled with fonts, colors, and sizing options. The library supports chainable methods for cleaner code, and all standard PDF drawing operations.

Building a Template-Driven System

The real power comes from separating content from layout. Instead of hardcoding positions, we define reusable templates.\

The Template Concept

A template is a declarative definition of your PDF’s layout - describing what goes where, how it should look, and what data fields it expects. By storing this as structured data (JSON, database records, etc.), you can modify layouts without touching code, enable version control, and allow non-technical team members to create new designs.\

Here’s what a template looks like:\

template-structure.json
{
"id": "book-cover-template",
"name": "Children's Book Cover",
"pageSize": "A4",
"pages": [
{
"pageNumber": 1,
"elements": [
{
"type": "image",
"field": "coverImage",
"x": 0,
"y": 0,
"width": 595,
"height": 842,
"fit": "cover"
},
{
"type": "text",
"field": "bookTitle",
"x": 100,
"y": 600,
"width": 395,
"fontSize": 48,
"font": "Helvetica-Bold",
"color": "#FFFFFF",
"align": "center"
},
{
"type": "text",
"field": "authorName",
"x": 100,
"y": 680,
"width": 395,
"fontSize": 24,
"font": "Helvetica",
"color": "#FFFFFF",
"align": "center"
}
]
}
]
}

Separating Content from Presentation

The generator class reads the template and loops through pages and elements. For each element, it extracts the corresponding data field and renders it using PDFKit methods. A text element uses doc.text() with position and styling from the template. An image element uses doc.image() with the file path from the data. This architecture means you can create multiple templates (certificates, posters, books) without changing the generator code.\

Dynamic Content Injection

With this system, generating a PDF becomes simple:\

usage-example.js
const template = require('./templates/book-cover-template.json');
const generator = new PDFGenerator(template);
const bookData = {
coverImage: '/path/to/cover-image.png',
bookTitle: 'The Adventures of Luna',
authorName: 'Written by Alex'
};
await generator.generate(bookData, 'output.pdf');

Practical Implementation Patterns

Let’s explore how to build a production-ready system with real-world considerations.\

Headless CMS Integration

Using a headless CMS (content management systems like Payload CMS, Directus, or Strapi) gives you a visual admin interface where your team can create and manage templates without writing code. These systems provide databases, APIs, and user interfaces out of the box.\

Instead of storing templates as flat JSON, structure them with proper relationships and nested data:\

template-collection-schema.ts
// Example schema for a CMS collection
export const Templates = {
slug: 'pdf-templates',
fields: [
{
name: 'name',
type: 'text',
required: true
},
{
name: 'category',
type: 'select',
options: ['book-cover', 'certificate', 'poster', 'custom']
},
{
name: 'pageSize',
type: 'select',
options: ['A4', 'Letter', 'Custom'],
defaultValue: 'A4'
},
{
name: 'pages',
type: 'array',
fields: [
{
name: 'pageNumber',
type: 'number',
required: true
},
{
name: 'elements',
type: 'array',
fields: [
{
name: 'type',
type: 'select',
options: ['text', 'image', 'shape', 'line']
},
{
name: 'fieldName',
type: 'text',
admin: {
description: 'Data field this element will display'
}
},
{
name: 'x',
type: 'number',
required: true
},
{
name: 'y',
type: 'number',
required: true
},
{
name: 'width',
type: 'number'
},
{
name: 'height',
type: 'number'
},
{
name: 'fontSize',
type: 'number',
admin: {
condition: (data, siblingData) => siblingData.type === 'text'
}
},
{
name: 'fontFamily',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData.type === 'text'
}
},
{
name: 'color',
type: 'text',
defaultValue: '#000000'
}
]
}
]
},
{
name: 'previewImage',
type: 'upload',
relationTo: 'media'
}
]
}

Benefits of this approach:\

Template Preview System

Building a preview system is crucial for validating templates before production use. Create an API endpoint that generates the PDF in memory, converts the first page to an image (using libraries like pdf-poppler or pdf2pic), and returns it to the frontend. Your team can then see exactly what the PDF will look like with sample data before deploying templates to production. This visual feedback loop dramatically speeds up template development and reduces errors.

File Management Strategy

For production systems, decide early whether to store PDFs locally or in cloud storage like AWS S3, Google Cloud Storage, or Azure Blob Storage. Create a storage abstraction layer that supports both options through environment configuration. This lets you develop locally but deploy with cloud storage. Consider organizing files by date or customer ID for easier management, and implement automatic cleanup policies for temporary or expired PDFs.

Real-World Considerations

Here are lessons learned from running a PDF generation system in production.\

Performance Optimization

The biggest performance bottleneck in PDF generation is typically image processing. Large, unoptimized images can dramatically increase both generation time and final file size.\

Image Optimization Strategy:
The key is to process images before adding them to the PDF. Tools like Sharp provide fast, efficient image manipulation:\

Implementation Approach:
Before rendering each image element in your template, pass it through Sharp to resize and convert to JPEG. Store the optimized version in a temporary location, use it in the PDF, then clean up afterward. Enable PDF compression in PDFKit’s document options (compress: true). Track all temporary files and ensure cleanup happens even if generation fails.\

Results You Can Expect:\

Key Principles:\

Font Management

Custom fonts are essential for branding and multilingual support. PDFKit uses doc.font(fontPath) to register fonts - you need the actual .ttf or .otf files on your server. Create a font registry that maps font names to file paths, supporting different weights (regular, bold, italic). For RTL languages like Arabic or Hebrew, use the features option with ['rtla'] to enable right-to-left text rendering, and set align: 'right' in text options. Load all fonts at startup to avoid repeated file reads.

Error Handling

Implement validation before generation starts. Check that templates have required structure (pages array, elements for each page). Validate that incoming data contains all required fields referenced in the template. Catch specific errors like missing image files (ENOENT), invalid fonts, or corrupted templates, and throw custom errors with clear messages and error codes. This helps with debugging and provides meaningful feedback to users.\

Scalability

For high-volume PDF generation (hundreds or thousands per day), implement a queue system using Redis and libraries like Bull or BullMQ. Instead of generating PDFs synchronously during API requests, add jobs to a queue and process them with worker processes. This prevents timeouts, allows retry logic for failures, and lets you scale horizontally by adding more workers. Track job progress and notify users when their PDF is ready.

Example Use Case: Personalized Children’s Books

Let me walk through a complete example of generating personalized books:\

The Requirements:\

Template Structure:\

book-template.json
{
"id": "personalized-book-adventure",
"name": "Personalized Adventure Book",
"pageSize": [595, 842],
"pages": [
{
"pageNumber": 1,
"type": "cover",
"elements": [
{
"type": "image",
"field": "backgroundImage",
"x": 0,
"y": 0,
"width": 595,
"height": 842
},
{
"type": "image",
"field": "childPhoto",
"x": 197.5,
"y": 200,
"width": 200,
"height": 200,
"shape": "circle"
},
{
"type": "text",
"field": "bookTitle",
"x": 50,
"y": 450,
"width": 495,
"fontSize": 36,
"font": "CustomBold",
"color": "#FFFFFF",
"align": "center"
}
]
},
{
"pageNumber": 2,
"type": "story",
"elements": [
{
"type": "image",
"field": "page2Illustration",
"x": 50,
"y": 50,
"width": 495,
"height": 400
},
{
"type": "text",
"field": "page2Text",
"x": 50,
"y": 470,
"width": 495,
"fontSize": 18,
"font": "CustomRegular",
"lineHeight": 1.5,
"align": "left"
}
]
}
]
}

The Process:
The template defines the cover page with a background image, centered child’s photo, and personalized title. Story pages follow a consistent pattern: illustration on top, text below. Data preparation fetches story content from the database, replaces placeholder tags like {childName} with the actual name, and maps each page’s illustration and text to the template’s field names. The generation flow is straightforward: fetch order → prepare data → load template → generate PDF → save to storage → notify customer.\

Results:
This system successfully generates personalized books with:\

Conclusion & Key Takeaways

Building a PDF generation system with PDFKit requires initial investment in architecture, but pays dividends in control, performance, and maintainability.\

When PDFKit is the Right Choice:\

Key Lessons Learned:\

  1. Invest in templates early - Separating layout from code makes iterations much faster
  2. Build preview systems - Visual feedback is essential for template development
  3. Optimize images - Image processing is often the bottleneck
  4. Plan for scale - Use queues for high-volume generation
  5. Error handling matters - Detailed validation prevents production issues

What I’d Do Differently:\

Resources to Get Started:\

The combination of PDFKit’s power with a well-architected template system creates a PDF generation solution that scales from dozens to thousands of documents while remaining maintainable and accessible to your entire team.