pdfkit
PDFKit is an imperative PDF generator for Node.js and browser bundles. You create a document stream, place wrapped text, embedded fonts, JPEG or PNG images, vector paths, tables, links, forms, outlines, encryption settings, and accessibility structure, then finalize the stream into a PDF. It gives low-level drawing control and some layout helpers, but it is not an HTML/CSS renderer and does not paginate an existing web page for you.
PDFKit is a capable choice when the PDF itself is the canvas and streaming, fonts, drawing, and document features matter. Choose an HTML renderer or declarative system when page layout is the problem, because PDFKit makes that work yours.
Use it if
- You generate invoices, reports, tickets, labels, or certificates from structured data and want exact drawing control
- You need streaming output to a file or HTTP response rather than launching a browser process
- You need embedded fonts, vector graphics, tables, annotations, AcroForms, outlines, encryption, or Tagged PDF in one library
- You can own page flow and visual regression tests instead of expecting browser layout rules
- Your source of truth is an HTML page or CSS template: PDFKit has no HTML/CSS layout engine, so Puppeteer is usually the shorter and more faithful route
- You want a small browser dependency: 0.19.1 is about 8.4 MB unpacked before your bundler, with font, line-breaking, PNG, hashing, and cipher dependencies
- Your layout requires automatic reflow of complex components, floats, grid, footnotes, and running sections: the README still lists higher-level layout APIs as future work
- You need first-party TypeScript declarations: the published 0.19.1 package metadata exposes neither a types entry nor an exports map
- You cannot test fonts and accessibility in the deployment environment: missing font files, unsupported glyphs, incorrect logical structure, or an omitted doc.end() can produce broken or unfinished output
Setup reality
npm install pdfkit is enough for Node's CommonJS API, and 0.19.1 also publishes an ES module build. The document is a readable stream: pipe it to a writable file, HTTP response, buffer collector, or browser Blob stream before adding content, and always call doc.end() so cross-reference tables and downstream finish events are written. Output is incremental by default, which keeps memory down but means earlier pages cannot be edited after they flush. Enable bufferPages when total-page numbering or backfilling is required, accepting that every buffered page stays in memory. Custom fonts must be available as files or buffers at runtime; PDF/A requires embedded fonts rather than the built-in AFM standard fonts. Text and tables can add pages, but application code still owns headers, footers, orphan rules, section continuity, and brand layout. Browser use adds more work: the README documents Browserify, webpack, or the standalone build, requires a separate sink such as blob-stream, and warns Browserify users to install brfs because built-in font data is otherwise missing. The npm package has no declared TypeScript types. Treat user content carefully because links, attachments, forms, metadata, and encryption options alter the document, and password protection is not a substitute for access control. Accessibility is also manual: Tagged PDF needs logical structure, meaningful reading order, alt text, language, and artifact marking; merely calling text and image does not make a compliant PDF. Finally, PDF rendering differs across Acrobat, browsers, macOS Preview, and print systems, so use fixture PDFs or rasterized visual comparisons and test real fonts, long text, page breaks, and large data sets before production.
Patterns
Stream a PDF to a filewrite-pdf-file
const PDFDocument = require('pdfkit');
const fs = require('node:fs');
const doc = new PDFDocument({ size: 'A4', margin: 50 });
doc.pipe(fs.createWriteStream('invoice.pdf'));
doc.fontSize(20).text('Invoice');
doc.end();Call end after adding all content; the writable stream finishes only after PDFKit writes the document trailer.
Send a generated PDF over HTTPstream-http-response
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=report.pdf');
const doc = new PDFDocument();
doc.pipe(res);
doc.text('Quarterly report');
doc.end();Set headers before piping and handle stream errors or client disconnects; generation failures after headers cannot become a normal JSON error response.
Flow text into columnswrap-multicolumn-text
doc.font('Times-Roman').fontSize(11).text(body, {
width: 495,
columns: 2,
columnGap: 24,
align: 'justify',
paragraphGap: 8,
});PDFKit advances doc.x and doc.y as it lays out text; measure or reset position before placing unrelated content afterward.
Register and reuse embedded fontsregister-custom-font
doc.registerFont('Body', 'assets/Inter-Regular.ttf');
doc.registerFont('Body Bold', 'assets/Inter-Bold.ttf');
doc.font('Body').fontSize(11).text('Normal text');
doc.font('Body Bold').text('Important text');Font files must exist in the deployed filesystem or be passed as buffers; embedding the actual fonts is required for PDF/A.
Fit an image inside a boxplace-image
doc.image('assets/logo.png', 50, 60, {
fit: [160, 80],
align: 'center',
valign: 'center',
});PDFKit directly supports JPEG and PNG; convert SVG, WebP, and other formats or draw them through supported vector paths.
Draw a filled and stroked shapedraw-vector-card
doc.save()
.roundedRect(50, 160, 495, 90, 8)
.fillAndStroke('#f8fafc', '#94a3b8')
.fillColor('#0f172a')
.fontSize(14)
.text('Account summary', 70, 185)
.restore();save and restore isolate fill, stroke, and transform state; forgetting them lets style changes bleed into later content.
Render a table with fixed and flexible columnsrender-table
doc.table({
columnStyles: [120, '*', 80],
rowStyles: (row) => row === 0 ? { backgroundColor: '#e2e8f0' } : {},
data: [
['SKU', 'Description', 'Amount'],
['A-10', 'Annual plan', '$120.00'],
],
});Tables can paginate, but test long cells, spans, fonts, and header behavior against the exact 0.19.x output you ship.
Add content whenever a page is createdadd-page-header
const doc = new PDFDocument({ margin: 60 });
doc.on('pageAdded', () => {
doc.fontSize(9).fillColor('gray').text('ACME REPORT', 60, 30, { align: 'right' });
doc.fillColor('black');
});The initial page exists before this listener runs; draw its header separately or create the document with autoFirstPage: false.
Backfill total page numbersadd-page-numbers
const doc = new PDFDocument({ bufferPages: true });
// Add all document content here.
const range = doc.bufferedPageRange();
for (let i = range.start; i < range.start + range.count; i += 1) {
doc.switchToPage(i);
doc.text(`Page ${i + 1} of ${range.count}`, 50, 780, { align: 'center' });
}
doc.end();bufferPages retains every page until flush or end, which increases memory use for large reports.
Create linked textadd-clickable-link
doc.fillColor('blue').text('View order online', {
link: 'https://example.com/orders/42',
underline: true,
});
doc.fillColor('black');Treat URLs as untrusted input and reset link or style options before subsequent text, especially when using continued segments.
Add a paragraph to the logical structuretag-accessible-paragraph
const doc = new PDFDocument({ lang: 'en-US', tagged: true });
doc.addStructure(doc.struct('P', () => {
doc.text('This paragraph participates in the reading order. ');
}));Tagged output requires all meaningful content in a correct structure and decorative content marked as artifacts; one tagged paragraph is not PDF/UA compliance.
Generate a downloadable PDF in the browsercreate-browser-blob
const PDFDocument = require('pdfkit');
const blobStream = require('blob-stream');
const doc = new PDFDocument();
const stream = doc.pipe(blobStream());
doc.text('Browser-generated PDF');
doc.end();
stream.on('finish', () => {
const url = stream.toBlobURL('application/pdf');
window.open(url);
});blob-stream is a separate dependency, and Browserify also needs brfs to include PDFKit's built-in font data.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pdf-lib | npm | You need to create and modify existing PDFs in JavaScript with a modern object-oriented API |
| jspdf | npm | Your priority is straightforward client-side PDF creation and a large plugin ecosystem |
| puppeteer | npm | Your document is naturally expressed as HTML and CSS and Chromium deployment is acceptable |
| @react-pdf/renderer | npm | You prefer declarative React components and stylesheet-driven document layout |