How to Generate PDFs in Node.js: The Complete Guide (2026)
PDF generation is one of those problems that sounds simple until you actually try it. You need pixel-perfect layouts, consistent fonts across operating systems, proper Unicode support, and a solution that does not choke under load. If you are building with Node.js, the ecosystem offers several approaches, each with genuine trade-offs.
This guide walks through every mainstream option available in 2026, compares them on real-world criteria, and helps you pick the right tool for your project.
1. PDFKit: The Low-Level Workhorse
PDFKit is a pure JavaScript library that generates PDFs by drawing directly onto a canvas-like surface. It gives you fine-grained control over every element: text positioning, vector graphics, embedded images, and custom fonts.
import PDFDocument from "pdfkit";
import fs from "node:fs";
const doc = new PDFDocument();
doc.pipe(fs.createWriteStream("output.pdf"));
doc.fontSize(24).text("Invoice #1042", 50, 50);
doc.fontSize(12).text("Amount: $420.00", 50, 90);
doc.end();Strengths
- Zero native dependencies. Pure JavaScript that runs anywhere Node runs.
- Extremely lightweight. No headless browser required.
- Full control over every pixel of output.
Weaknesses
- No HTML or CSS support. You are essentially programming a PDF by hand, placing each element with x/y coordinates.
- Complex layouts (tables, multi-column, page-break logic) require significant custom code.
- Maintenance burden grows fast as document complexity increases.
Best for: simple, highly customized documents where you need total control and want to avoid heavy dependencies.
2. Puppeteer and Playwright: The Browser Approach
Puppeteer (Chrome) and Playwright (Chrome, Firefox, WebKit) spin up a headless browser, render an HTML page, and export the result as PDF. This is the most popular approach because developers already know HTML and CSS.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(`
<html>
<body>
<h1>Invoice #1042</h1>
<p>Amount: $420.00</p>
</body>
</html>
`);
const pdf = await page.pdf({
format: "A4",
printBackground: true,
});
await browser.close();Strengths
- Use HTML and CSS you already know. Flexbox, Grid, web fonts all work.
- Accurate rendering because it uses a real browser engine.
- Can render any web content, including charts and SVGs.
Weaknesses
- Heavy resource usage. Each Chromium instance consumes 100-300 MB of RAM. At scale, this becomes your primary infrastructure cost.
- Cold start time of 1-3 seconds per browser instance. Connection pooling helps but adds complexity.
- Deployment complexity. You need Chromium binaries installed on your server, which complicates Docker images and serverless deployments.
- Page break behavior is inconsistent across browser versions and requires careful CSS tuning.
Best for: teams that already have HTML templates and need to generate PDFs at low to moderate volume.
3. jsPDF: Client-Side Generation
jsPDF generates PDFs entirely in the browser (or in Node). It is commonly paired with html2canvas to convert rendered DOM elements into images that get placed on a PDF page.
import { jsPDF } from "jspdf";
const doc = new jsPDF();
doc.setFontSize(20);
doc.text("Invoice #1042", 20, 30);
doc.setFontSize(12);
doc.text("Amount: $420.00", 20, 50);
const buffer = doc.output("arraybuffer");Strengths
- Works in the browser. No server required for simple cases.
- Lightweight bundle compared to headless browser solutions.
Weaknesses
- The html2canvas approach converts DOM to rasterized images, producing blurry text that is not selectable or searchable.
- Limited CSS support. Complex layouts often break.
- Not suitable for server-side generation at scale because the API is designed around browser DOM primitives.
Best for: simple client-side exports where users need a quick download and quality requirements are relaxed.
4. pdf-lib: Manipulating Existing PDFs
pdf-lib is a pure JavaScript library focused on modifying existing PDF files: filling form fields, merging documents, adding watermarks, or inserting pages.
import { PDFDocument, StandardFonts } from "pdf-lib";
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([600, 400]);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText("Invoice #1042", {
x: 50,
y: 350,
size: 24,
font,
});
const bytes = await pdfDoc.save();Strengths
- Excellent for PDF manipulation: merge, split, fill forms, add watermarks.
- Pure JavaScript, works in browser and Node.
- Strong TypeScript support.
Weaknesses
- Not designed for creating complex documents from scratch.
- No HTML/CSS rendering. Same coordinate-based positioning as PDFKit.
Best for: workflows that modify, merge, or annotate existing PDF files rather than creating new ones from scratch.
5. wkhtmltopdf: The Legacy Option
wkhtmltopdf is a command-line tool (with Node wrappers) that uses the QtWebKit engine to render HTML to PDF. It was the go-to solution for years before Puppeteer existed.
Strengths
- Fast rendering for simple HTML pages.
- Lightweight compared to full Chromium.
Weaknesses
- Uses an outdated rendering engine. Modern CSS (Grid, Flexbox, custom properties) often breaks or renders incorrectly.
- The project is effectively unmaintained. The last major release was in 2020 and open issues continue to pile up.
- Security vulnerabilities in the underlying Qt libraries.
- Difficult to install on modern Linux distributions and in containers.
Best for: legacy systems that already depend on it. Avoid for new projects.
6. Cloud PDF APIs
Instead of running PDF infrastructure yourself, cloud APIs let you send data or HTML and receive a finished PDF back. You offload rendering, font management, scaling, and browser maintenance to the API provider.
This approach makes the most sense when you do not want to manage Chromium servers, need consistent output across environments, or your volume is unpredictable and you want pay-per-document pricing.
const response = await fetch("https://api.rendoc.dev/v1/render", {
method: "POST",
headers: {
"Authorization": "Bearer your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
template: "invoice",
data: {
number: "1042",
amount: "$420.00",
customer: "Acme Corp",
},
}),
});
const pdf = await response.arrayBuffer();Strengths
- No infrastructure to manage. No Chromium, no font issues, no Docker headaches.
- Consistent rendering across all environments.
- Scales automatically. You do not provision servers for peak load.
- Template management built in. Designers can update templates without deploying code.
Weaknesses
- Network dependency. Your documents are generated via HTTP, so latency exists.
- Ongoing cost. Free tiers cover small volume, but high-volume users pay per document.
- Data leaves your network, which may not work for highly regulated industries.
Comparison Table
| Criteria | PDFKit | Puppeteer | jsPDF | pdf-lib | Cloud API |
|---|---|---|---|---|---|
| HTML/CSS support | None | Full | Limited | None | Full |
| Server resources | Low | High | Low | Low | None |
| Setup complexity | Low | High | Low | Low | Minimal |
| Output quality | Excellent | Excellent | Fair | Excellent | Excellent |
| Scale without ops | No | No | N/A | No | Yes |
How to Choose
Start with what matters most for your project:
- Need full layout control with HTML/CSS? Puppeteer or a cloud API. Puppeteer if you want to self-host; a cloud API if you do not want the infrastructure overhead.
- Simple documents, minimal dependencies? PDFKit gives you the lightest footprint.
- Merging, filling forms, watermarking? pdf-lib is purpose-built for PDF manipulation.
- Unpredictable volume? Cloud APIs scale to zero when idle and handle spikes without provisioning.
Using rendoc for Node.js PDF Generation
If the cloud API approach fits your use case, rendoc is designed specifically for developers who want PDF generation without infrastructure management. You get:
- A REST API that accepts JSON data and returns a PDF in under 2 seconds.
- Template management with a visual editor, so non-developers can update layouts.
- Full HTML and CSS support, including web fonts and responsive layouts.
- A generous free tier for prototyping and low-volume use.
npm install rendoc
import { Rendoc } from "rendoc";
const rendoc = new Rendoc({ apiKey: process.env.RENDOC_API_KEY });
const pdf = await rendoc.render({
template: "invoice",
data: {
number: "1042",
items: [
{ description: "Consulting", amount: 420 },
],
},
});
// pdf is a Buffer ready to send, save, or streamThe SDK handles retries, timeouts, and streaming responses. You focus on your application logic; rendoc handles the PDF rendering pipeline.
Whether you choose a low-level library, a headless browser, or an API depends on your specific constraints: team size, volume, compliance requirements, and how much infrastructure you want to own. The good news is that the Node.js ecosystem gives you solid options at every level.