Kapom Report is the easy way to build jsPDF 4.x + jspdf-autotable 5.x reports — grouped tables, subtotals, and multi-page layouts in a few declarative lines, in Node or the browser. No manual x/y cursor math, strict TypeScript, Thai-ready.
It solves the usual pain points of hand-rolled jsPDF reports: manual x/y cursor tracking, page-break bookkeeping, and re-deriving the same table/group/subtotal logic on every report. Multi-page (100+ pages) reports are a first-class case, not an afterthought.
Where it runs — the same API everywhere. kapom-report is framework-agnostic: use it from a Node.js backend / API / serverless function to stream a PDF in an HTTP response, or from any frontend framework — Vue, React, Angular, Svelte, or plain HTML — via a bundler. It's not tied to any one framework; the core builds the PDF, and you decide what to do with the bytes.
npm install kapom-report jspdf jspdf-autotable
jspdf/jspdf-autotable are peer dependencies — kapom-report doesn't bundle them.
The core (building a PDF and reading its bytes with doc.output(...)) runs anywhere — Node, the browser via a bundler, and edge/worker runtimes.
The save() / preview() convenience methods do file I/O, so they need one of:
process.getBuiltinModule, added in that release. On an older Node they throw a clear, actionable error instead of crashing.save() triggers a download, preview() opens a new tab.On any other runtime (an older Node, Deno, Bun, React Native, an edge runtime), skip save()/preview() and write the bytes yourself — it's one line and works everywhere:
import { writeFileSync } from 'node:fs';
const report = createKapomReport({ columns, data });
writeFileSync('report.pdf', Buffer.from(report.doc.output('arraybuffer')));
The same doc.output('arraybuffer') is all you need to serve a PDF from an API — e.g. an Express/Fastify route:
app.get('/report.pdf', (_req, res) => {
const report = createKapomReport({ columns, data });
const bytes = Buffer.from(report.doc.output('arraybuffer'));
res.setHeader('Content-Type', 'application/pdf').send(bytes);
});
import { col, reportBuilder } from 'kapom-report';
interface Sale {
product: string;
qty: number;
}
const c = col<Sale>(); // column constructors bound to the row type
reportBuilder<Sale>()
.title('Monthly Sales Report')
.table({
columns: [
c.data('product', 'Product'),
c.data('qty', 'Qty', { align: 'right', aggregate: 'sum' }),
],
data: [
{ product: 'Widget', qty: 12 },
{ product: 'Gadget', qty: 7 },
],
})
.save('report.pdf');
No font setup, no jsPDF/AutoTable knowledge required — this renders a titled table with a summed "Total" row out of the box. Prefer an object config? The same report is createKapomReport({ title, columns, data }).save('report.pdf').
New to the layout? Report anatomy maps every region of a report to the reportBuilder() method that produces it.
// Sale = { product: string; category: string; qty: number }; sales: Sale[]; c = col<Sale>()
reportBuilder<Sale>()
.table({
columns: [
c.data('product', 'Product'),
c.data('qty', 'Qty', { align: 'right', aggregate: 'sum' }),
],
data: sales,
group: 'category', // shorthand — or ['region', 'category'] for nested groups
})
.save('sales.pdf');
Each group gets a header band and a subtotal row; the whole table gets a grand total. Groups can nest to any depth via GroupResolver.subGroup, and keep-together rules stop a group's header from ever being stranded alone at the bottom of a page.
Standard PDF fonts (helvetica, times, etc.) have no Thai glyphs — kapom-report fails fast with a clear error instead of silently producing mojibake. Register a real font to render Thai (or any script the font supports):
import { readFileSync } from 'node:fs';
import { col, reportBuilder } from 'kapom-report';
interface Person {
name: string;
}
const c = col<Person>();
reportBuilder<Person>()
.font({
// register any TTF — Sarabun here covers Thai; use a CJK / Arabic / etc. font for those scripts
fonts: [{ family: 'Sarabun', data: new Uint8Array(readFileSync('Sarabun-Regular.ttf')), style: 'normal' }],
})
.table({
columns: [c.data('name', 'Name')],
data: [{ name: 'Preecha' }], // renders any Unicode the registered font supports
})
.save('report.pdf');
The library follows progressive disclosure everywhere a config exists:
numberFormat → 2 decimal places; no font → works fine for Latin scripts).group: 'category', { key, header } instead of a full column object, title: '...' instead of a manual text block).ReportNode); drop down to the full shape any time you need more control.Everything ultimately compiles to the same ReportNode tree, which is the single source of truth the render engine walks — the fluent/shorthand API never diverges from what you could write by hand.
{pageNumber}, {totalPages}, {date}, ...), watermarks, a ReportRegistry for composing multi-section reportsnode: imports outside the Node I/O boundary)any, noUncheckedIndexedAccess + exactOptionalPropertyTypes cleanSee examples/ for 14 runnable demos (numbered basic → advanced) covering every feature above — run any of them directly with npx tsx examples/<name>/index.ts, or npm run demo to generate all of them at once into examples/output/.
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm test # vitest run
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
npm run demo # generate every example PDF into examples/output/
MIT